Java异常体系#
一句话答案#
Throwable 分 Error(不可恢复)和 Exception,Exception 分 Checked(编译时检查)和 Unchecked(RuntimeException)。
核心要点
体系结构:
Throwable(唯一能被 throw/catch 的根)
├── Error(JVM 级严重错误,程序不该捕获)
│ ├── OutOfMemoryError
│ └── StackOverflowError
└── Exception
├── Checked(编译期强制处理:IOException、SQLException…)
└── RuntimeException(Unchecked:NPE、IndexOutOfBounds、ClassCast…)plaintextChecked vs Unchecked 的设计哲学与争议:
- Checked 体现”可恢复的、调用方必须正视”的契约——编译器强制 try/catch 或 throws。
- Unchecked 代表编程错误,本应靠改代码消灭,不该强制捕获。
- 争议:Checked 异常常被批评破坏封装、催生
catch + 打日志 + 吞掉的坏代码,C#/Kotlin 干脆取消了 Checked;Lambda/Stream 里 Checked 异常也很别扭。
机制深挖:
- 异常构造为什么贵 ——
fillInStackTrace():new Exception()在构造时会回填整个调用栈(遍历栈帧、记录类名/方法/行号),这是 native 操作,开销远大于抛出/捕获本身。高频抛异常(如用异常做控制流)会成为性能热点。优化:重写fillInStackTrace()返回 this(不抓栈),或用单例/静态预建异常(牺牲栈轨迹换性能,Netty 等框架就这么干)。 - 字节码层的异常表(exception table):
try-catch不产生额外指令,编译器为每个方法生成一张异常表,每条记录是[起始PC, 结束PC, 处理器PC, 捕获类型]。抛异常时 JVM 拿当前 PC 去逐条匹配异常表找 catch 块,匹配不到就出栈到上层方法继续找——这就是”晚捕”在底层的样子。 - try-with-resources 的字节码展开:编译器把它展开成 try-finally,finally 里按声明逆序调用
close()。若 try 体和 close() 都抛异常,主异常保留、close 的异常被addSuppressed()挂为 suppressed exception(getSuppressed()可取),避免异常被覆盖丢失。
最佳实践: 早抛晚捕(在能处理的层捕获)、绝不空 catch 吞异常、捕获要具体不要 catch(Exception)、业务错误用自定义异常携带错误码、异常不用于正常控制流。
面试回答(2分钟版)
Java异常体系的根类是Throwable,下面分两大分支:Error和Exception。Error代表JVM级别的严重错误,比如OutOfMemoryError和StackOverflowError,程序不应该也无法捕获处理。Exception又分为Checked异常和Unchecked异常:Checked异常是编译器强制要求处理的,比如IOException和SQLException,必须try-catch或throws声明;Unchecked异常即RuntimeException及其子类,比如NullPointerException、ArrayIndexOutOfBoundsException,编译器不强制处理,通常代表编程逻辑错误。实际开发中推荐用try-with-resources代替手动关闭资源,它编译后会自动生成finally块调用close方法。finally块几乎一定执行,除了System.exit、JVM崩溃、守护线程被终止这几种极端情况。需要注意finally中的return会覆盖try中的return,这是一个坏实践应该避免。异常处理的原则是具体异常具体捕获,不要用大而化之的catch Exception吞掉所有异常导致问题被隐藏。
追问与易错
追问方向:
- “Error 和 Exception 的区别?”→ Error 是 JVM 级别严重错误(如 OOM、StackOverflow),程序不应捕获处理;Exception 是程序可处理的异常,分 Checked(编译时强制处理)和 Unchecked(RuntimeException)
- “try-with-resources 底层怎么实现的?”→ 编译器自动生成 finally 块调用资源的 close() 方法,若 try 和 close 都抛异常,close 的异常作为 suppressed exception 附加到主异常上
- “finally 一定会执行吗?”→ 几乎一定执行,除三种情况:调用 System.exit() 终止 JVM、JVM 崩溃或被 kill -9、守护线程在 JVM 退出时被强制终止
易错点:
- ❌ “finally 中 return 覆盖 try 中 return”——会的,但这是坏实践
- ❌ 随意使用 catch(Exception e) 吞异常——应该具体异常具体处理