本节摘要:Throwable 是差错登记簿总账,分 Error(系统级灾难,业务不处理)与 Exception(可处理差错);Exception 再分运行时异常(非受检,多是代码缺陷)与受检异常(环境风险,编译器强制处理)。这条受检边界决定了"要么 try 要么 throws"的语法压力从何而来。本节立起族谱分层图,把常见差错逐个归位,并解释编译器为什么只盯受检一侧。
第 6 章写流的时候,每个方法后面都拖着一个 throws IOException——那会儿先当固定骨架用,现在拆开。程序运行中会出两种状况:可预期的差错(文件不存在、网络断了、输入格式不对)与不可预期的崩溃(把对象引用当数组用、栈溢出)。Java 的处理方式是把差错本身也做成对象——差错发生时,构造一个异常对象、带着发生地点与原因的堆栈信息抛出来,正常执行流就地中断,交给"接得住"的代码处理。这套机制的顶层就是 Throwable:能被抛出的一切都是它的后代。
public class WhatDemo { static int divide(int a, int b) { return a / b; // b 为零时 抛 ArithmeticException 差错对象就此诞生 } public static void main(String[] args) { System.out.println(divide(10, 2)); // 输出:5 System.out.println(divide(10, 0)); // 抛出 ArithmeticException 程序中断 System.out.println("这行执行不到"); // 中断点之后的代码不再执行 } }
不处理时,运行时把异常对象的堆栈打印到控制台(就是常见的那串"异常名、位置、调用链"),程序终止。堆栈信息是排错的第一现场:第一行是差错类型,往下是调用链,从发生点一路指回入口。

两层四格各自的责任,用"归位练习"最见效。下面每个差错都该能落进唯一一格:
| 差错 | 归哪格 | 处理责任 |
|---|---|---|
| 空指针异常 | 运行时异常 | 修代码:判空或保证赋值 |
| 数组下标越界 | 运行时异常 | 修代码:检查边界 |
| 类转换异常(2.2 节强转失败) | 运行时异常 | 修代码:instanceof 先行 |
| 算术异常(除零) | 运行时异常 | 修代码:分母校验 |
| 文件不存在 | 受检异常 | 要么提示重选路径 要么先建文件 |
| 输入输出异常(读写失败) | 受检异常 | 重试或回滚并告知调用方 |
| 中断异常(线程被打断,第 8 章) | 受检异常 | 响应中断 收尾退出 |
| 内存溢出错误 | Error | 业务接不住 调整内存或修资源泄漏 |
归位表的规律一句话:运行时异常是"你写错了",受检异常是"世界出状况了"。前者的正解是修代码而不是加 try(用 try 把空指针吞掉,等于把缺陷埋进地毯下面);后者的正解是预案——文件不存在给用户提示、网络断了重试,这些状况写得再好也可能发生,必须有人接。
受检异常的处理压力来自编译器的一条硬规矩:方法里可能抛出受检异常时,要么就地 try 接管,要么在签名上 throws 声明上报,二选一,缺一不编译。而运行时异常与 Error 不受此约束——它们可以随时出现、无需声明(当然也可以显式声明,只是编译器不强制)。
import java.io.FileInputStream; import java.io.FileNotFoundException; public class CheckDemo { // 写法一:throws 上报 —— 我不接 谁调用谁负责 static void loadA(String path) throws FileNotFoundException { FileInputStream in = new FileInputStream(path); // 不声明就编译报错 System.out.println("打开成功"); } // 写法二:try 接管 —— 我自己处理 static void loadB(String path) { try { FileInputStream in = new FileInputStream(path); System.out.println("打开成功"); } catch (FileNotFoundException e) { System.out.println("接管:路径不对 已提示用户重选"); } } public static void main(String[] args) throws FileNotFoundException { loadA("不存在的文件.bin"); // 抛出 FileNotFoundException 堆栈打印在控制台 loadB("不存在的文件.bin"); // 输出:接管:路径不对 已提示用户重选 } }
两种写法编译都过,行为大不相同:loadA 把处理责任上交调用方(最终没人接就在控制台打印堆栈并终止);loadB 就地消化、程序继续跑。这条边界的工程意义是逼人做决定:文件不存在是可预见的,编译器不许你"假装看不见"——要么给答案(try),要么把问题转给更有能力的人(throws)。设计上这是把"错误处理责任"变成 API 契约的一部分,调用者看方法签名就知道它会抛什么状况。
顺带把两个易混点钉死。其一,运行时异常同样可以 try:框架层统一接空指针并记日志是合法用法,只是别用它替代修缺陷。其二,Error 不是 Exception 的子类(两者都直接继承 Throwable)——catch Exception 接不住内存溢出,这是好事:接不住的灾难硬接只会更糟。
分类清楚了,下一节讲处置流程:try-catch-finally 的执行顺序(return 撞上 finally 谁说了算)、多个 catch 的匹配规则,以及替你自动关流的"带资源的 try"。