Skip to content
第 29 / 250 章后端⏱ 10 分钟阅读

第 29 章:异常处理基础

学习目标

  • 理解异常体系(Error / Exception / RuntimeException)
  • 掌握 try-catch-finally 执行顺序
  • 学会 try-with-resources
  • 学会自定义异常

一、为什么需要异常?

java
// ❌ 用返回值表示错误:调用方容易忽略
public int divide(int a, int b) {
    if (b == 0) return -1;      // -1 是错误码还是正常结果?
    return a / b;
}

// ✅ 用异常:调用方必须处理,且错误信息完整
public int divide(int a, int b) {
    if (b == 0) throw new ArithmeticException("除数不能为 0");
    return a / b;
}

核心价值:把「正常逻辑」和「错误处理」分离,让主流程保持清晰。

二、异常体系

受检 vs 非受检

维度受检异常(Checked)非受检异常(Unchecked)
代表IOExceptionSQLExceptionRuntimeException 及其子类
编译器强制处理(try 或 throws)不强制
语义外部环境问题(文件不存在、网络断开)程序 bug(空指针、越界)
建议能恢复就捕获,不能就包装上抛修 bug,别用 catch 掩盖
java
// 受检异常:必须处理,否则编译不过
public void readFile() throws IOException {   // ① 声明抛出
    Files.readString(Path.of("a.txt"));
}

// 非受检异常:不用声明
public void parse(String s) {
    Integer.parseInt(s);   // 可能抛 NumberFormatException(RuntimeException 子类)
}

三、try-catch-finally

java
public static int read(String path) {
    try {                                       // ① 可能出错的代码
        return Integer.parseInt(path);
    } catch (NumberFormatException e) {         // ② 捕获具体异常
        System.out.println("格式错误:" + e.getMessage());
        return -1;
    } catch (Exception e) {                     // ③ 兜底(必须放最后)
        return -2;
    } finally {                                 // ④ 无论如何都执行
        System.out.println("清理资源");
    }
}

执行顺序

关键finally 一定执行(除非 System.exit() 或 JVM 崩溃)。

坑:finally 里 return 会吞掉异常

java
// ❌ 永远返回 1,异常被吞掉
public int bad() {
    try {
        throw new RuntimeException("出错了");
    } finally {
        return 1;       // ❌ 覆盖了异常
    }
}

规约finally不要写 return / break / continue

多异常合并捕获(JDK 7+)

java
try {
    doSomething();
} catch (IOException | SQLException e) {   // ① 用 | 分隔
    log.error("IO 或 SQL 出错", e);
}

四、try-with-resources(JDK 7+)

java
// ❌ 老写法:嵌套 finally,啰嗦且容易漏关
BufferedReader br = null;
try {
    br = new BufferedReader(new FileReader("a.txt"));
    System.out.println(br.readLine());
} catch (IOException e) {
    e.printStackTrace();
} finally {
    if (br != null) {
        try { br.close(); } catch (IOException ignored) { }
    }
}

// ✅ 新写法:自动关闭
try (BufferedReader br = new BufferedReader(new FileReader("a.txt"))) {  // ①
    System.out.println(br.readLine());
} catch (IOException e) {
    e.printStackTrace();
}
// ② 离开 try 块自动调用 br.close()

原理:资源类实现了 AutoCloseable 接口,编译器自动插入 close() 调用。

java
public class MyResource implements AutoCloseable {
    public void use() { System.out.println("使用资源"); }

    @Override
    public void close() {                 // ① 实现 close
        System.out.println("资源已关闭");
    }
}

try (MyResource r = new MyResource()) {
    r.use();
}
// 输出:使用资源 → 资源已关闭

规约:所有 IO 流、数据库连接、网络连接,一律用 try-with-resources。

五、抛出异常

java
// throw:抛出一个异常对象
public void setAge(int age) {
    if (age < 0) {
        throw new IllegalArgumentException("年龄不能为负:" + age);
    }
    this.age = age;
}

// throws:声明方法可能抛出的异常
public void readConfig() throws IOException {
    Files.readString(Path.of("config.yml"));
}
关键字位置作用
throw方法体内实际抛出一个异常对象
throws方法签名上声明「我可能抛这些异常,调用方注意」

六、自定义异常

java
// ① 继承 RuntimeException(业务异常推荐,不强制调用方 try)
public class BusinessException extends RuntimeException {

    private final int code;                        // ② 携带业务错误码

    public BusinessException(int code, String message) {
        super(message);                            // ③ 交给父类保存 message
        this.code = code;
    }

    public BusinessException(int code, String message, Throwable cause) {
        super(message, cause);                     // ④ 保留原始异常链,排查必备
        this.code = code;
    }

    public int getCode() { return code; }
}

使用

java
public User getUser(Long id) {
    User user = userMapper.selectById(id);
    if (user == null) {
        throw new BusinessException(40401, "用户不存在: " + id);
    }
    return user;
}

为什么继承 RuntimeException 而不是 Exception? 业务异常通常由全局异常处理器统一兜底(见第 51 章),不需要每一层都 try-catch,否则代码会被 try 淹没。

异常链:不要丢掉 cause

java
// ❌ 丢失原始堆栈,线上排查抓瞎
try {
    jdbc.query(sql);
} catch (SQLException e) {
    throw new BusinessException(50000, "查询失败");   // ❌ e 被丢弃
}

// ✅ 保留原始异常
try {
    jdbc.query(sql);
} catch (SQLException e) {
    throw new BusinessException(50000, "查询失败", e);  // ✅
}

七、异常处理规约

规约说明
不要捕获 Throwable / Error虚拟机级错误,捕获也没法恢复
不要空 catchcatch (Exception e) {} 等于把 bug 藏起来
不要用异常控制流程异常创建要抓堆栈,比 if 慢几十倍
catch 要具体先具体后宽泛,Exception 放最后
打日志用 log.error("msg", e)第二参数传异常对象才有堆栈
不要 e.printStackTrace()生产环境输出到 stderr,不进日志文件
java
// ❌ 用异常做流程控制(慢!)
try {
    Integer.parseInt(s);
    return true;
} catch (NumberFormatException e) {
    return false;
}

// ✅ 用判断
return s != null && s.matches("-?\\d+");

八、本章小结

要点关键
ThrowableError(别管) + Exception(要管)
受检异常编译器强制处理
非受检异常RuntimeException,通常是 bug
finally一定执行,别在里面 return
try-with-resources自动关闭,IO 首选
自定义异常业务异常继承 RuntimeException,带错误码
异常链一定要传 cause

动手练习

练习 1:基础题

预测输出顺序:

java
public static int test() {
    try {
        System.out.println("A");
        return 1;
    } finally {
        System.out.println("B");
    }
}
// 调用 test(),输出什么?返回什么?

练习 2:进阶题

实现一个 BusinessException,包含错误码、错误消息、时间戳,并写一个静态工厂方法 BusinessException.of(ErrorCode code, Object... args) 支持消息模板填充。


下一章第 30 章:异常最佳实践

本站基于 VitePress 构建 · 由 Codebook 团队维护