Skip to content

Chapter 13 · 异常处理

程序写得再好,也躲不过"网络断了、文件不见了、用户乱输入"这些事 — 异常机制就是 Java 的"应急预案"。


🎯 本章目标

  • 理解 异常的本质:错误传递机制
  • 区分 检查异常 vs 非检查异常
  • 掌握 try-catch-finallytry-with-resources
  • 会用 throwthrows
  • 学会自定义异常

1. 没有异常会怎样?

c
// C 语言风格:用返回值判断错误
int result = divide(10, 0);
if (result == ERROR_DIV_ZERO) {
    // 处理
}

问题:

  • 错误信息少(只有错误码,没有上下文)
  • 容易忽略(忘了检查返回值就当没事发生)
  • 代码满是 if 判断

Java 用 异常机制 解决:错误自动"抛出来",不处理就崩,强制你面对。


2. 异常体系

异常类层级

Throwable
├── Error              → 严重错误,程序无法处理(OutOfMemoryError、StackOverflowError)
└── Exception          → 一般错误
    ├── RuntimeException     → 非检查异常(运行时异常)
    │   ├── NullPointerException
    │   ├── ArrayIndexOutOfBoundsException
    │   ├── ClassCastException
    │   ├── IllegalArgumentException
    │   └── ArithmeticException
    └── 其他 Exception 子类  → 检查异常(编译期强制处理)
        ├── IOException
        ├── SQLException
        └── ClassNotFoundException

检查异常 vs 非检查异常

类型必须处理?例子设计意图
Checked ExceptionIOException、SQLException可恢复的、外部环境问题
Unchecked Exception(RuntimeException)NPE、IndexOutOfBoundsException程序员的 bug,应该修复
ErrorOOM、StackOverflowErrorJVM 的事,应用搞不定

3. try-catch-finally

try-catch-finally 流程

java
try {
    // 可能出错的代码
    int x = 10 / 0;
} catch (ArithmeticException e) {
    // 处理这个异常
    System.out.println("除零了:" + e.getMessage());
} catch (Exception e) {
    // 兜底
    System.out.println("其他异常");
} finally {
    // 不管有没有异常,都会执行
    System.out.println("收尾工作");
}

多 catch(注意顺序)

子类异常必须写在父类前面!否则永远进不去子类的 catch。

java
try { ... }
catch (FileNotFoundException e) { }     // 子类(先)
catch (IOException e) { }                // 父类(后)

JDK 7+ 支持 多重 catch(|)

java
try { ... }
catch (IOException | SQLException e) {
    // 两种异常一起处理
}

finally 一定会执行吗?

几乎一定。除非:

  • JVM 崩了(System.exit(0)
  • 线程被 kill -9
  • 死循环 / 无限阻塞

面试坑

java
int test() {
    try { return 1; }
    finally { return 2; }   // 返回 2!finally 覆盖 try 的返回值
}

4. try-with-resources(推荐)

JDK 7+ 引入,自动关闭资源(替代笨重的 finally):

java
// ❌ 老写法
BufferedReader br = null;
try {
    br = new BufferedReader(new FileReader("a.txt"));
    return br.readLine();
} catch (IOException e) {
    e.printStackTrace();
} finally {
    if (br != null) {
        try { br.close(); } catch (IOException e) { ... }
    }
}

// ✅ 新写法(自动关闭)
try (BufferedReader br = new BufferedReader(new FileReader("a.txt"))) {
    return br.readLine();
} catch (IOException e) {
    e.printStackTrace();
}

要求:资源必须实现 AutoCloseable 接口(绝大多数 IO 类都实现了)。


5. throw vs throws

关键字位置作用
throw方法体内抛出 一个异常对象
throws方法签名上声明 这个方法可能抛出的异常
java
public void readFile(String path) throws IOException {  // 声明
    if (path == null) {
        throw new IllegalArgumentException("path 不能为 null");  // 抛出
    }
    // ... 可能抛 IOException
}

调用方必须处理

java
void useIt() {
    try {
        readFile("a.txt");        // 必须 try-catch
    } catch (IOException e) { ... }
}

// 或者继续往上抛
void useIt() throws IOException {
    readFile("a.txt");
}

6. 自定义异常

业务异常通常自定义:

java
public class InsufficientBalanceException extends RuntimeException {
    private double current;
    private double required;

    public InsufficientBalanceException(double current, double required) {
        super(String.format("余额不足,当前 %.2f, 需要 %.2f", current, required));
        this.current = current;
        this.required = required;
    }

    public double getCurrent() { return current; }
    public double getRequired() { return required; }
}

💡 怎么选父类

  • 业务可恢复 → 继承 RuntimeException(推荐,调用方自愿处理)
  • 必须强制处理 → 继承 Exception

7. 异常处理最佳实践

java
// ❌ 吞掉异常(最严重的反模式)
try { ... } catch (Exception e) {}

// ❌ 用 e.printStackTrace() 当日志(生产环境别这样)
catch (Exception e) { e.printStackTrace(); }

// ❌ 抓到异常啥也不做
catch (Exception e) { /* TODO */ }

// ❌ 抓 Throwable / Error
catch (Throwable t) { ... }

// ✅ 1. 用日志框架
catch (IOException e) {
    log.error("读取配置失败", e);
    throw new ConfigLoadException("无法启动", e);
}

// ✅ 2. 抛具体异常,不要包装太宽
throw new UserNotFoundException("用户 " + id + " 不存在");

// ✅ 3. 异常链:保留原始原因
throw new ServiceException("失败", originalException);

8 大军规

  1. 不要吞异常
  2. catch 写得越小越好(别一上来就 catch Exception)
  3. finally 不要 return
  4. 优先用 try-with-resources
  5. 业务异常用 RuntimeException 子类
  6. 保留异常链new BusinessException("msg", originalException)
  7. 日志记录 + 抛新异常,不要又记又抛重复
  8. 不要用异常控制业务流程(性能差,可读性差)

8. 实战练习

文件内容
BasicExceptionDemo.javatry-catch-finally
TryWithResourcesDemo.java自动关闭资源
CustomException.java自定义业务异常
BankExceptionDemo.java银行业务异常实战

9. 浏览器演示

打开 demo.html

  • 异常类层级图
  • 触发各种异常体验
  • finally 执行顺序

10. 面试可能会问什么?

Q1: 检查异常和非检查异常的区别?

  • 检查异常:编译期强制处理(catch 或 throws),通常表示外部环境问题
  • 非检查异常(RuntimeException 及其子类):编译期不强制,通常表示程序 bug

Q2: try-catch-finally 中 finally 一定会执行吗?

几乎一定。除非:

  • JVM 退出(System.exit)
  • finally 块前有死循环或无限阻塞
  • 当前线程被 kill

Q3: throw 和 throws 区别?

  • throw方法体内 抛出异常对象
  • throws方法签名 上声明异常

Q4: NullPointerException 怎么避免?

  1. 提前判空
  2. Objects.requireNonNull(obj, "msg")
  3. Optional
  4. JDK 14+:开启 -XX:+ShowCodeDetailsInExceptionMessages,NPE 直接告诉你哪个变量是 null
  5. 谨慎用基本类型包装类

Q5: try 块里有 return,finally 会执行吗?

。finally 在 return 之前执行:

java
int test() {
    try { return 1; }       // ① 计算 1,存到返回寄存器
    finally { System.out.println("finally"); }  // ② 执行
    // ③ 真正 return 1
}

Q6: try-with-resources 的关闭顺序?

逆序:后声明的资源先关闭。

java
try (Resource a = ...; Resource b = ...) {
    // 先关 b 再关 a
}

Q7: 自定义异常应该继承谁?

  • 业务异常 / 调用方有时不想处理 → RuntimeException
  • 想强制调用方处理(如 IO) → Exception

现代 Java 倾向 RuntimeException(Spring/Hibernate 都这么干)。


🎁 本章小结

✅ 异常 = 程序的"应急预案"
✅ Checked 编译期强制处理;Unchecked 不强制
✅ try-catch-finally:finally 几乎一定执行
✅ try-with-resources 自动关闭,优先用
✅ throw 抛出,throws 声明
✅ 自定义异常优先继承 RuntimeException

🔗 导航