五、异常处理(J45–J50)
J45 · 知识点:异常体系
【题目】Java 中 Throwable 的直接分类是? A. 错误(Error)和异常(Exception) B. 受检异常(Checked)和非受检异常(Unchecked) C. IOException 和 SQLException D. 运行时异常和编译时异常,且两者编译时都必须捕获
答案:A
【考点】Throwable 顶层分类:Error 与 Exception 及其子分类。
【结论】选 A。Throwable 的直接子类仅 Error 与 Exception,二者构成 Java 异常体系最顶层二分。
【逐项辨析】
- B错:受检/非受检是 Exception 的子分类,不是 Throwable 的直接分类。
- A正确:Throwable 直接分为 Error(JVM 级错误)和 Exception(程序可处理异常)。
- C错:IOException 与 SQLException 只是受检异常的具体例子,并非顶层分类。
- D错:运行时异常与编译时异常并非标准术语,且 RuntimeException 编译期不强制捕获。
【知识点】 Java 异常体系以 Throwable 为根,直接派生 Error 与 Exception 两大分支。Error 表示 JVM 层面的严重问题(如 OutOfMemoryError、StackOverflowError),程序通常无法也不应捕获。Exception 表示程序运行中的可恢复异常,其下再分为:
| 分类 | 继承关系 | 编译期行为 | 典型示例 |
|---|---|---|---|
| 受检异常 | Exception 且非 RuntimeException 子类 | 强制 try-catch 或 throws | IOException、SQLException |
| 非受检异常 | RuntimeException 及其子类 | 不强制处理 | NullPointerException、ArrayIndexOutOfBoundsException |
Error 与 Exception 同为 Throwable 的直接子类,这是异常体系中最顶层的划分。受检/非受检则是 Exception 之下的二级划分,不能替代 Error 与 Exception 的顶层地位。
【记忆锚点】 「Throwable 生了俩:Error 绝症,Exception 感冒;感冒还分看不看医生(Checked / Unchecked)。」
【易混对比】
- 易混:题目问「Throwable 的直接分类」时,B 选项极具干扰性——受检/非受检属于 Exception 的二级分类,而非 Throwable 的直接子类(本题正确答案是 A:Error 与 Exception)。
- 换问法:若问「编译期强制要求处理的异常属于哪一类?」则答案为 Checked Exception(受检异常)。
【自测】 以下哪个类不是 Throwable 的直接子类? A. Exception B. Error C. RuntimeException D. OutOfMemoryError
答:选 C(RuntimeException 挂在 Exception 之下,不是 Throwable 的直接子类)。注意 D 项「Error 与 Exception 同级」本身也成立——所以这道自测若照原样问会有两个正解;这里按「谁不是 Throwable 的直接子类」的口径答 C,实测
OutOfMemoryError.getSuperclass()=VirtualMachineError,Error 与 Exception 才是 Throwable 的直接子类。与第 J45 题连考。
【知识关联】
- 同库关联:与 J46–J50 异常题群;Error vs Exception vs RuntimeException 边界。
- 实现层:Throwable 子类;受检异常由编译器强制;运行时异常不强制捕获。
- 面试追问:① 何时自定义受检异常?② Error 该不该捕获?
【拓展延伸】
- 变式问法:ClassNotFoundException 与 NoClassDefFoundError 区别。
- 版本差异:语义稳定;多异常捕获 JDK7+ 语法。
- 工程注意点:不要吞异常;业务异常 vs 系统异常分层;保留 cause。
J46 · 知识点:finally 块
【题目】关于 finally 块的说法,正确的是?
A. finally 块在任何情况下都会执行 B. 当 catch 或 try 中执行了 System.exit(0) 时,finally 不会执行 C. finally 中不能出现 return 语句 D. finally 中的 return 不会覆盖 try 中的 return
答案:B
【考点】finally 的执行时机与 System.exit 的例外。
【结论】选 B。finally 在正常情况下必定执行,但 System.exit(0) 直接终止 JVM 进程,finally 随之被跳过。
【逐项辨析】
- A 错:finally 并非绝对执行,System.exit()、JVM 崩溃、线程被强制终止(kill -9)时均不执行。
- B 正确:System.exit(0) 立即终止 JVM,finally 块不会得到执行机会。
- C 错:finally 中不仅可以写 return,语法上完全合法。
- D 错:finally 中的 return 会覆盖 try / catch 中的返回值,这是 Java 的明确规定,也是编码大忌。
【知识点】 finally 的设计目的是保证资源清理或收尾逻辑必定执行,其执行时机为 try 块正常结束、catch 块捕获异常后、或 return 语句真正返回之前。但存在以下例外场景导致 finally 不执行:
- 调用 System.exit(int) 或 Runtime.halt(int) 直接终止 JVM 进程;
- JVM 自身崩溃(如 SIGKILL 信号、操作系统级别杀进程);
- 执行 finally 的线程被中断或杀死。
此外,finally 中若包含 return,会抑制 try / catch 中的异常抛出,并覆盖先前的返回值,极易隐藏 Bug,因此编码规范(如《阿里巴巴 Java 开发手册》)明确禁止在 finally 中使用 return。
【记忆锚点】 「finally 不是万能保险,exit 杀进程全完蛋;finally 里别 return,覆盖了值还吞异常。」
【易混对比】
- 易混:return 在 try 中并不会立即返回,而是先执行 finally;若 finally 无 return,再带回 try 中的返回值;若 finally 有 return,则 try / catch 的返回值和异常都被忽略。
- 换问法:若问「以下哪种情况 finally 一定不执行?」应选 System.exit(0);若问「以下哪种写法会抑制异常?」应选 finally 中写 return。
【自测】 以下代码返回什么?
public static int test() {
try {
return 1;
} finally {
return 2;
}
}答:返回 2。finally 中的 return 覆盖了 try 中的 return,最终返回 2。与第 J46 题连考。
【知识关联】
- 同库关联:与 J50(try-with-resources)、P63(Python 的 finally 与 return,同型陷阱)、面试高频「finally 是否执行」;J63 讲的是运行时数据区,不要错引成 finally。
- 实现层:finally 在正常/异常/return 前都会插入;若 finally 也 return 会覆盖。
- 面试追问:① finally 中 return 的陷阱?② System.exit 时 finally?
【拓展延伸】
- 变式问法:try return 后 finally 改变量是否影响返回值(基本类型已计算)。
- 版本差异:语义稳定。
- 工程注意点:finally 只做清理不写 return;资源关闭用 try-with-resources。
J47 · 知识点:多 catch 顺序
【题目】一个 try 后跟多个 catch 块时,正确的排列顺序是? A. 子类异常在前,父类异常在后 B. 父类异常在前,子类异常在后 C. 顺序无关紧要 D. 一个 try 只能有一个 catch
答案:A
【考点】catch 的匹配顺序与“不可达 catch”编译规则。
【结论】选 A。catch 按书写顺序自上而下匹配,子类异常必须放在父类异常之前,否则后面的 catch 将永远无法到达。
【逐项辨析】
- A正确:先捕获子类(如 IOException),再捕获父类(如 Exception),保证每个 catch 都有可达路径。
- B错:若父类在前,编译器会报「exception has already been caught」,子类 catch 不可达,编译失败。
- C错:顺序至关重要,Java 编译器会检查 catch 块的可达性,父类写在前面时子类 catch 编译不过。
- D错:一个 try 后可以跟多个 catch,这是 Java 异常处理的标准语法。
【知识点】 Java 编译器对 catch 块的检查遵循「可达性分析」规则。由于异常匹配采用「is-a」关系(即子类异常同样满足父类 catch 的捕获条件),一旦某个 catch 捕获了父类异常,其后的所有子类 catch 都将被判定为不可达代码(unreachable code),导致编译错误。
正确顺序示例:
try {
// ...
} catch (FileNotFoundException e) { // 子类在前
// ...
} catch (IOException e) { // 父类在后
// ...
} catch (Exception e) { // 更顶层父类
// ...
}同级异常(如 IOException 与 SQLException 互为兄弟类)之间顺序可以互换,因为不存在父子覆盖关系。
【记忆锚点】 「catch 排队先小后大,子在前父在后,反过来编译都不过。」
【易混对比】
- 易混:catch(Exception e) 放在第一个位置是常见笔试陷阱,它会导致后面所有具体异常 catch 都编译报错。
- 换问法:若问「以下代码能否编译通过?」并给出父类在前、子类在后的 catch 序列,答案应为「不能编译,子类 catch 不可达」。
【自测】 以下代码能否编译通过?
try {
int a = 1 / 0;
} catch (Exception e) {
System.out.println("Exception");
} catch (ArithmeticException e) {
System.out.println("ArithmeticException");
}答:不能编译通过。Exception 在前捕获了所有异常,后面的 ArithmeticException catch 不可达,编译报错。与第 J47 题连考。
【知识关联】
- 同库关联:与 J45 体系、J49 自定义异常继承链。
- 实现层:catch 按顺序匹配,父类 catch 会屏蔽后面的子类 catch(编译错误或不可达)。
- 面试追问:① 多 catch 顺序规则?② JDK7 multi-catch 限制?
【拓展延伸】
- 变式问法:先 catch Exception 后 catch IOException 能否编译。
- 版本差异:JDK7 支持
catch (A | B e),不能有子父关系。 - 工程注意点:先具体后宽泛;统一异常处理见 Spring S12。
J48 · 知识点:throw 与 throws
【题目】关于 throw 和 throws 的说法,正确的是?
A. throw 用于方法声明,throws 用于抛出异常对象 B. throw 在方法体内抛出异常对象,throws 在方法签名上声明可能抛出的异常类型 C. 两者可以互换使用 D. throws 用于创建异常对象
答案:B
【考点】throw(抛出实例)与 throws(声明类型)的区别。
【结论】选 B。throw 是在方法体内抛出异常实例的动作,throws 是在方法签名上声明可能抛出异常类型的语法。
【逐项辨析】
- A错:throw 用于抛出实例,throws 用于方法声明,两者职责相反。
- B正确:throw 后跟异常对象(如 throw new IOException());throws 后跟异常类型列表(如 throws IOException, SQLException)。
- C错:二者语法位置、作用完全不同,绝对不能互换。
- D错:throws 不创建对象,仅做类型声明;创建异常对象由 throw 配合 new 完成。
【知识点】 throw 与 throws 是 Java 异常处理机制的两个不同维度:
| 维度 | throw | throws |
|---|---|---|
| 语法位置 | 方法体内部 | 方法签名尾部 |
| 作用 | 实际产生并抛出一个异常对象 | 声明方法可能抛出的异常类型 |
| 次数 | 每次只能抛出一个实例 | 可以声明多个异常类型,逗号分隔 |
| 强制性 | 受检异常对象必须通过 throw 或内部调用产生 | 受检异常必须声明或内部 try-catch 处理,否则编译失败 |
受检异常(Checked Exception)的传播链条:方法内抛出或内部方法传递来的受检异常,要么在当前方法内 try-catch 处理,要么通过 throws 继续向上抛给调用方处理,否则编译失败。
【记忆锚点】 「throw 是动手扔(new 对象),throws 是贴告示(声明类型);throw 在里,throws 在外。」
【易混对比】
- 易混:throws 声明的异常类型必须与 throw 抛出的实例类型匹配(或为其实例的父类)。方法声明 throws Exception,实际可以 throw new IOException(),因为 IOException 是 Exception 的子类。
- 换问法:若问「方法声明 throws IOException,但方法体内没有 throw 语句,是否合法?」答案是合法——因为 throws 也可以用来声明内部调用的其他方法可能传递过来的异常。
【自测】 以下方法声明是否有编译错误?
public void read() throws IOException {
throw new FileNotFoundException("file missing");
}答:无编译错误。FileNotFoundException 是 IOException 的子类,子类异常实例可以匹配父类 throws 声明。与第 J48 题连考。
【知识关联】
- 同库关联:与 J45/J49;与 Spring @Transactional rollbackFor(S14)——checked 异常默认不回滚。
- 实现层:throws 是方法签名声明;throw 是抛出动作;字节码 athrow。
- 面试追问:① throws 与 throw 区别?② 重写方法 throws 规则?
【拓展延伸】
- 变式问法:构造器 throws;接口方法异常声明。
- 版本差异:语义稳定。
- 工程注意点:API 文档写清受检异常;运行时异常优先用于编程错误。
J49 · 知识点:自定义异常
【题目】若要定义自定义的受检异常(Checked Exception),应继承? A. RuntimeException B. Exception(且不是 RuntimeException 的子类) C. Error D. Throwable
答案:B
【考点】自定义异常的继承选择与受检/非受检的划分。
【结论】选 B。自定义受检异常应直接继承 Exception(且不属于 RuntimeException 体系),编译期才会强制调用方处理。
【逐项辨析】
- A错:继承 RuntimeException 得到的是非受检异常,编译期不强制捕获或声明。
- B正确:继承 Exception(非 RuntimeException 子类)即成为受检异常,符合题目要求。
- C错:Error 代表 JVM 级严重错误,不应被应用程序继承用作业务异常。
- D不选(并非功能错误):直接继承 Throwable 同样能编译,且抛出的自定义异常仍强制调用方 catch/throws——JDK 17 实测不处理会报「未报告的异常错误 MyEx2; 必须对其进行捕获或声明以便抛出」,按 JLS 它确实属于受检异常。问题在于它既不在 Exception 分支也不在 Error 分支(实测
Exception.class.isAssignableFrom(MyEx)为 false),与框架、工具链按 Exception/Error 分类的约定冲突,语义不清。因此本题只取 B(继承 Exception)这一规范写法,D 是「能用但不该用」。
【知识点】 Java 异常的受检/非受检属性由继承链决定:
| 继承目标 | 异常类型 | 编译期行为 | 典型用途 |
|---|---|---|---|
| Exception(非 RuntimeException 子类) | 受检异常 | 强制 try-catch 或 throws | 可恢复的外部错误(IO、SQL) |
| RuntimeException | 非受检异常 | 不强制处理 | 编程错误(空指针、越界) |
| Error | 错误 | 不强制处理 | JVM 内部致命错误 |
| Throwable | 根类 | 不推荐使用 | 一般不应直接继承 |
自定义异常的最佳实践:
- 受检异常 → 继承 Exception;
- 非受检异常 → 继承 RuntimeException(或 IllegalArgumentException 等已有子类);
- 提供带 String message 和 Throwable cause 的构造方法,便于异常链传递。
【记忆锚点】 「要受检就认 Exception 爹,不要 RuntimeException 这个干爹;认 Error 当爹是自寻死路。」
【易混对比】
- 易混:RuntimeException 本身继承自 Exception,但 RuntimeException 及其所有子类都被 JVM 特殊对待,归为 Unchecked。因此「继承 Exception」不等于「受检」,必须是「继承 Exception 但不是 RuntimeException 的子类」才构成受检异常。
- 换问法:若问「如何定义一个编译期不强制处理的自定义异常?」则应答「继承 RuntimeException」。
【自测】 以下自定义异常中,哪一个是受检异常?
class A extends Exception {}
class B extends RuntimeException {}
class C extends Throwable {}
class D extends Error {}答:选 A(规范答案)。A 直接继承 Exception 且不在 RuntimeException 体系内,是标准受检异常;B 是非受检异常;D 属 Error 体系,不该用于业务异常。注意 C(直接继承 Throwable)在编译器看来同样必须 catch 或 throws,功能上也算受检异常,只是游离在 Exception/Error 两大分支之外,属于「能编译可用但不符合规范」的写法。与第 J49 题连考。
【知识关联】
- 同库关联:与 J45/J48;工程上常做 BusinessException。
- 实现层:继承 Exception(受检)或 RuntimeException(非受检)。
- 面试追问:① 如何设计异常体系?② serialVersionUID 作用?
【拓展延伸】
- 变式问法:自定义异常必须调用 super(message)?
- 版本差异:语义稳定。
- 工程注意点:携带错误码;不要用异常做流程控制;日志记录 cause。
J50 · 知识点:try-with-resources
【题目】JDK7 的 try-with-resources 语句要求资源类必须实现?
A. Serializable B. AutoCloseable(或其子接口 Closeable) C. Cloneable D. Runnable
答案:B
【考点】自动资源管理(ARM)的接口约束与关闭顺序。
【结论】选 B。try-with-resources 要求资源类实现 AutoCloseable 接口(或其子接口 Closeable),JVM 在退出 try 块时自动调用 close()。
【逐项辨析】
- A 错:Serializable 是序列化标记接口,与资源关闭无关。
- B 正确:AutoCloseable 是 try-with-resources 的强制接口约束,Closeable 继承自 AutoCloseable。
- C 错:Cloneable 是对象克隆标记接口,与资源管理无关。
- D 错:Runnable 是线程执行接口,与资源关闭无关。
【知识点】 try-with-resources 是 JDK7 引入的自动资源管理机制,语法为 try (Resource res = new Resource()) { ... }。其工作原理:
- 资源必须实现 AutoCloseable 接口(仅声明 void close() throws Exception);
- Closeable extends AutoCloseable,因此所有传统 IO 流(InputStream / OutputStream / Reader / Writer)均满足条件;
- try 块正常结束或发生异常退出时,JVM 自动按声明的逆序调用各资源的 close();
- 若 close() 也抛出异常,该异常会被抑制(suppressed),附加到主异常上,可通过 Throwable.getSuppressed() 获取。
多个资源声明示例:
try (BufferedReader br = new BufferedReader(new FileReader("a.txt"));
BufferedWriter bw = new BufferedWriter(new FileWriter("b.txt"))) {
// 使用 br、bw
} // 先关闭 bw,再关闭 br【记忆锚点】 「try 后面加括号,AutoCloseable 来把关;先进后出逆序关,异常抑制挂旁边。」
【易混对比】
- 易混:传统 try-finally 也能关闭资源,但 try-with-resources 更简洁且能处理 close() 异常 suppression。对比:若 finally 中 close() 抛异常,会覆盖 try 块的主异常;而 try-with-resources 会保留主异常,将 close 异常标记为 suppressed。
- 换问法:若问「try-with-resources 中多个资源的关闭顺序是什么?」应答「与声明顺序相反(逆序关闭)」。
【自测】 以下代码中哪个类可以作为 try-with-resources 的资源?
class A implements AutoCloseable { public void close() {} }
class B implements Closeable { public void close() {} }
class C implements Runnable { public void run() {} }答:A 和 B 都可以。A 直接实现 AutoCloseable;B 实现 Closeable(Closeable 继承 AutoCloseable);C 实现 Runnable,不满足条件。与第 J50 题连考。
【知识关联】
- 同库关联:与 J46(finally)、J65/J63 资源与内存;与 P65(Python with)跨语言对照。
- 实现层:JDK7+;AutoCloseable;编译为 try/finally 调用 close,抑制异常用 addSuppressed。
- 面试追问:① 多个资源关闭顺序?② close 异常如何处理?
【拓展延伸】
- 变式问法:括号内多个资源;自定义 AutoCloseable。
- 版本差异:JDK9+ 允许 effectively final 已有资源变量复用。
- 工程注意点:IO/连接一律 try-with-resources;不要手动 close 忘 finally。