Skip to content

异常处理 ​

Java 初级 · 进阶基础

程序运行中难免出错:文件不存在、网络中断、除零、空指针……Java 用「异常机制」把这些意外统一管理,让你在出错时优雅兜底,而不是整个程序崩溃。本节不只讲 try-catch 怎么写,更从 Throwable 体系、字节码异常表(exception table)、栈展开(stack unwinding)、try-with-resources 如何被翻译成字节码 讲透异常的本质与代价。

一、Throwable 体系:谁该被捕获 ​

所有异常的根是 Throwable,分两支:

  • Error:JVM 层面的严重故障(如 OutOfMemoryError、StackOverflowError),通常不应也不该捕获——程序已无能为力。
  • RuntimeException(非受检):程序逻辑 bug 导致(空指针、越界、除零),编译不强制处理。正确做法是改代码避免,而非靠 catch 兜。
  • 受检异常(Checked):外部环境导致(IO 失败、SQL 异常),编译期强制 try-catch 或 throws,逼你正视「这可能失败」。

受检 vs 非受检的设计哲学

受检异常用于「调用方必须处理或显式声明」的、可预期的外部失败(如文件打不开);非受检异常用于「本不该发生的编程错误」。滥用受检异常会让签名充斥 throws,所以现代做法(如 Spring)常把受检异常包装成非受检(RuntimeException)以减少噪音——但初学仍要分清两者。

二、try-catch-finally 的执行流转 ​

java
try {
    int[] a = {1};
    System.out.println(a[1]);        // 抛 ArrayIndexOutOfBoundsException
} catch (ArrayIndexOutOfBoundsException e) {
    System.out.println("越界:" + e.getMessage());   // 捕获处理
} finally {
    System.out.println("无论是否异常都执行");           // 收尾(关资源等)
}

执行顺序铁律:try 正常完成 → finally;try/catch 中遇到 return → 先执行 finally 再真正返回。finally 用于「无论如何都要做」的事(释放锁、关流)。

三、异常的底层:字节码异常表与栈展开 ​

try-catch 不是运行时逐层 if 判断,而是编译进 .class 的异常表(exception table):

text
Exception table:
  from  to    target  type
  0     5     8      Class ArrayIndexOutOfBoundsException
  // 若 PC 在 [0,5) 抛该类型异常,跳转到 PC=8 的 catch 处理

每个 try 块在字节码里对应异常表的一行:[start_pc, end_pc) 是监控范围,handler_pc 是 catch 起点,catch_type 是捕获的异常类。运行时若抛出异常:

  1. JVM 沿当前方法异常表找匹配项,命中就跳到 handler。
  2. 若当前方法没有匹配的 catch,JVM 把当前栈帧弹出(栈展开 stack unwinding),带着异常对象回到调用者,重复查找——这就是「异常一直向上冒泡」的底层过程。
  3. 直到某个方法 catch 住,或一路冒到 main 仍没人处理,线程终止并打印堆栈(printStackTrace 的逻辑)。

异常有性能代价

抛出异常要填充栈轨迹(stack trace)——遍历整个调用栈收集每一帧,开销不小。因此绝不要用异常做正常流程控制(比如用抛异常来「跳出循环」或替代 if 判断)。异常只用于真正的「意外错误」,频繁触发的路径用返回值/Optional 表达。

四、throws 与 throw ​

  • throw new XxxException("..."):手动抛出一个异常实例。
  • throws XxxException:在方法签名上声明「本方法可能抛出受检异常」,把处理责任交给调用方。
java
static int divide(int a, int b) throws ArithmeticException {
    if (b == 0) throw new ArithmeticException("除数不能为 0");
    return a / b;
}

自定义业务异常

用继承自 RuntimeException 的自定义异常表达业务错误(如 BalanceNotEnoughException),好处是不污染方法签名(非受检,无需到处 throws),调用方可自由选择是否捕获:

java
if (balance < cost) throw new BusinessException("用户 " + uid + " 余额不足");

异常信息要带上下文(如用户 ID),排错时才好定位。

五、try-with-resources:自动关资源的字节码真相 ​

Java 7+ 的 try-with-resources(TWR) 让实现 AutoCloseable 的资源(流、连接、Channel)在块结束自动关闭,无需手写 finally:

java
try (BufferedReader br = new BufferedReader(new FileReader("data.txt"))) {
    String line = br.readLine();
    System.out.println(line);
} catch (IOException e) {
    e.printStackTrace();
}

它编译后长什么样? ​

TWR 本质是编译器语法糖:编译后变成「在 try 外套 finally,依次调用 close(),并把关闭时的异常通过 addSuppressed 挂到主异常上」。大致等价:

java
BufferedReader br = new BufferedReader(new FileReader("data.txt"));
Exception ex = null;
try {
    ... 使用 ...
} catch (Exception e) {
    ex = e; throw e;
} finally {
    if (br != null) {
        if (ex != null) br.close();      // 关闭异常被 addSuppressed
        else br.close();
    }
}

AutoCloseable 接口只有一个方法 void close() throws Exception,任何资源类实现它就能享受 TWR。多个资源用 ; 分隔:try (A a = ...; B b = ...) 会按声明逆序关闭。

finally 里写 return 会吞异常

若 finally 中有 return,它会覆盖 try/catch 里的返回值,且会掩盖已抛出的异常(异常被丢弃,调用方毫无察觉)。永远不要在 finally 里 return,也不要在 finally 里抛异常掩盖主逻辑异常——这正是 TWR 用 addSuppressed 精心规避的问题。

六、常见误区与注意点 ​

WARNING

  • 捕获了却不处理:catch (Exception e) {} 空着,bug 被悄悄吞掉,极难排查。至少打日志或重新抛出。
  • catch 顺序写反:子类异常必须写在父类(Exception)之前,否则编译报错或永远命中宽泛分支、子类 catch 成死代码。
  • 用异常做正常流程控制:开销大,不要用 try-catch 代替 if 判断(如用异常判断是否越界)。
  • finally 里 return:覆盖返回值且吞掉异常,应避免。
  • 笼统 catch Exception 再忽略:过宽捕获会连 RuntimeException 也兜住,掩盖真正的编程错误;尽量 catch 具体类型。

七、最佳实践 ​

  1. 只捕获你能合理处理的异常,捕获具体类型而非宽泛 Exception。
  2. 资源一律用 try-with-resources 自动关闭,减少泄漏与样板代码。
  3. 业务异常自定义并继承 RuntimeException(或 Exception 视需),避免到处 throws。
  4. 异常信息带上下文(如「用户ID=123 余额不足」),便于排错;用 addSuppressed 保留关闭异常。
  5. 别用异常控制流程;别在 finally 写 return;catch 到就处理或明确向上抛,别静默吞掉。

📚 官方文档与参考资料 ​

✏️ 练习与自测 ​

  1. 写一个读取数组元素的方法,用 try-catch 捕获越界异常并打印友好提示;用 javap -c 查看生成的异常表(Exception table 段)。
  2. 定义 AgeException(继承 RuntimeException),当年龄 < 0 或 > 150 时抛出;在调用处用 try-catch 处理。
  3. 用 try-with-resources 读写一个文本文件,体会自动关闭;故意在 try 块里抛异常,再在资源 close() 里抛异常,观察 getSuppressed() 如何保留两个异常。
  4. 写一个方法故意在 finally 中 return,调用它并观察:已知 try 块会抛异常,调用方能拿到这个异常吗?解释为什么(finally return 吞异常)。

本节小结

异常体系:Error(不 catch,如 OOM)↙ Exception → RuntimeException(非受检,靠改代码避免)与受检异常(编译强制处理)。try-catch 底层是 .class 里的异常表(监控区间 + handler_pc + catch_type),抛异常时 JVM 查表,不匹配就栈展开逐层回退直到被 catch 或线程终止。throws 声明、throw 抛出;自定义业务异常继承 RuntimeException 更清爽。Java 7+ 的 try-with-resources 是语法糖:编译为 finally 中逆序调 close(),关闭异常经 addSuppressed 保留,避免资源泄漏与异常掩盖。异常有性能代价,勿作流程控制;finally 禁 return(吞异常)。