LESSON 06 · 异常处理

异常处理
try-catch / checked vs unchecked

这节我们啃 Java 异常处理:异常的继承体系try-catch-finally 三件套、面试常问的 checked vs unchecked 区别、还有 Java 7+ 的 try-with-resources 优雅写法。

学完这节:你能画出异常继承树、说清 RuntimeException 和其他 Exception 的本质区别、能用 try-with-resources 替代繁琐的 finally 关闭代码。
1

异常体系:Throwable 家族树

💡 动机:先把家族关系理清,再学单个异常就不乱

Java 所有异常都继承自 Throwable,分两族:Error(系统级)和 Exception(程序级)。

Throwable                         // 所有异常的祖宗
├── Error                         // 系统级错误,程序通常无法处理
│   ├── OutOfMemoryError          // 内存溢出
│   ├── StackOverflowError        // 栈溢出(递归太深)
│   └── ...
└── Exception                     // 程序级异常,可以/应该处理
    ├── IOException               // checked:必须 try-catch 或 throws
    ├── SQLException               // checked
    ├── ParseException             // checked
    └── RuntimeException           // ⭐ unchecked:可以不处理
        ├── NullPointerException
        ├── ArrayIndexOutOfBoundsException
        ├── ArithmeticException
        ├── ClassCastException
        ├── IllegalArgumentException
        ├── NumberFormatException
        └── ...
记忆口诀:
  • Error = "天灾"(OOM、栈溢出)→ 别想着 catch,没救
  • Exception = "人祸"(你代码写错了)→ 应该处理
  • RuntimeException = "逻辑 bug" → 应该改代码,不是 catch
2

try-catch-finally:异常处理三件套

💡 动机:怎么"抓住"异常不让程序崩?
try {
    // 可能抛异常的代码
    int result = 10 / 0;
} catch (ArithmeticException e) {
    // 抓住特定异常,处理
    System.out.println("除数不能为 0");
    e.printStackTrace();  // 打印堆栈(调试用)
} catch (Exception e) {    // 可以有多个 catch,从具体到宽泛
    System.out.println("其他异常:" + e.getMessage());
} finally {
    // 无论是否异常,都会执行(常用于释放资源)
    System.out.println("这行总会跑");
}
三件套要点:
  1. try 块:可能抛异常的代码
  2. catch 块:从具体到宽泛,不然编译警告(子类必须在父类前)
  3. finally 块:无论是否异常都跑(除了 System.exit()
  4. Java 7+ 可以 catch (IOException | SQLException e) 多异常一起抓

⭐ return 和 finally 的优先级

// 面试题:下面的方法返回什么?
public static int test() {
    try {
        return 1;
    } finally {
        return 2;   // ⚠️ finally 里的 return 会覆盖 try 里的
    }
}
// 答:返回 2(finally 优先级更高)

// 实际开发:不要在 finally 里 return!会吞掉异常
3

checked vs unchecked ⭐ 面试常问

💡 动机:面试必问——这两类异常到底怎么区分?
对比Checked(受检)Unchecked(非受检)
父类 Exception(不含 RuntimeException) RuntimeException 及其子类
编译器 ⭐ 强制处理(try-catch 或 throws) 不强制,可以不管
常见例子 IOException, SQLException, ParseException NPE, 数组越界, 除零, 类型转换
本质 外部环境问题(文件不存在、网络断了) 代码 bug(逻辑错误)
处理方式 必须处理或往上抛 应该改代码,不靠 catch
// checked:必须处理
public void readFile() throws IOException {  // 显式声明抛出
    Files.readAllBytes(Paths.get("a.txt"));
}

// unchecked:编译不报错,但运行会崩
public void npe() {
    String s = null;
    s.length();   // 编译过,运行时 NPE
}
⭐ 面试题:Error 和 Exception 区别?
  1. Error = 系统级,JVM 自己处理不了(如 OOM),程序无法恢复
  2. Exception = 程序级,可以/应该处理(catch 或 throws)
  3. 两者都继承自 Throwable,但 Error 不是 Exception 的子类
4

5 个最常见的 RuntimeException

💡 动机:认识常见异常,看到报错就知道改哪里

点下面 5 个按钮,看每种异常会怎么触发、长啥样:

// 选上面一个场景看代码和异常
5 个常见异常的"翻译":
  • NullPointerException = 空指针:对象是 null 却调用了方法/属性
  • ArrayIndexOutOfBoundsException = 数组越界:下标 < 0 或 ≥ length
  • ArithmeticException = 除零:整数除以 0(浮点除 0 得 Infinity)
  • ClassCastException = 类型转换失败(String) someObject 但它不是 String
  • NumberFormatException = 数字格式错Integer.parseInt("abc")
5

try-with-resources:优雅的资源关闭

💡 动机:原来关资源要写一堆 finally,Java 7 给了个简化写法

读写文件、网络连接、数据库连接……这些用完必须关闭,否则资源泄漏。

老写法(finally 关闭)

// ❌ 啰嗦版:每个资源都要 finally 关闭
BufferedReader br = null;
try {
    br = new BufferedReader(new FileReader("a.txt"));
    String line = br.readLine();
    System.out.println(line);
} catch (IOException e) {
    e.printStackTrace();
} finally {
    if (br != null) {
        try { br.close(); } catch (IOException e) { /* 忽略 */ }
    }
}

新写法(try-with-resources)

// ✅ 优雅版:Java 7+,资源自动关闭
try (BufferedReader br = new BufferedReader(new FileReader("a.txt"))) {
    String line = br.readLine();
    System.out.println(line);
} catch (IOException e) {
    e.printStackTrace();
}
// 出 try 块时,br 自动调用 close(),连 catch 都省了
使用条件:资源类必须实现 AutoCloseable 接口(Java 7+ 标准库里几乎所有资源类都已实现):
  • InputStream / OutputStreamReader / Writer
  • Connection / Statement / ResultSet(JDBC)
  • Socket

多资源用分号隔开:

try (InputStream in = new FileInputStream("a.txt");
     OutputStream out = new FileOutputStream("b.txt")) {
    // 两者都会自动关闭
}
6

自定义异常 + 最佳实践 ⭐ 面试常问

💡 动机:业务里"用户余额不足"、"订单不存在"这种业务错误怎么抛?

自定义异常 3 步

// 1. 继承 RuntimeException(业务异常一般是 unchecked)
public class InsufficientBalanceException extends RuntimeException {
    // 2. 提供构造器
    public InsufficientBalanceException(String message) {
        super(message);
    }
}

// 3. 在业务代码里抛
public void withdraw(double amount) {
    if (balance < amount) {
        throw new InsufficientBalanceException(
            "余额不足:需要 " + amount + ",余额 " + balance
        );
    }
    balance -= amount;
}

⭐ 异常处理 5 大最佳实践

  1. 能处理就处理,处理不了就抛出(不要 catch 了什么都不做)
  2. 异常信息要具体throw new IllegalArgumentException("age 不能为负: " + age),不是 throw new IllegalArgumentException("error")
  3. 不要用异常控制流程(try 里 for 循环,break 跳出来——这是反模式)
  4. 不要吞异常:catch 里至少 log.error(...) 一下
  5. 资源关闭用 try-with-resources,别写 finally 关闭

⭐ 经典面试题:try-catch-finally 执行顺序

// 问:try 里 return 了,finally 还跑吗?
public static int test() {
    try {
        return 1;
    } finally {
        System.out.println("finally 跑了");
        // return 2;  // ⚠️ 千万别这么写
    }
}
// 输出:先打印 "finally 跑了",然后返回 1
// 答:finally 在 return 之前 跑(修改 return 值除外)

你的作业(分步走)

  1. 触发 5 个常见异常:写代码故意触发 NullPointerExceptionArrayIndexOutOfBoundsExceptionArithmeticExceptionClassCastExceptionNumberFormatException 这 5 个,每个 catch 住并打印异常名 + 消息
  2. try-with-resources:写代码读 a.txt 第一行,用 try-with-resources(不是 finally),验证文件关闭正常
  3. 自定义异常:写个 IllegalAgeException extends RuntimeException,然后写个 register(int age) 方法,age < 0 或 > 150 抛异常
  4. catch 顺序:写代码故意先 catch Exception 再 catch NullPointerException,看编译报什么错——体会"先具体后宽泛"的规则
  5. 面试题答题:凭记忆写下"checked vs unchecked 的区别"3 条 + "finally 和 return 哪个先执行"答案,贴给我看