一、💡 一句话理解
核心结论
Java 中所有异常和错误都继承
Throwable。Error通常表示程序难以恢复的严重问题;Exception表示程序可能处理的问题,其中又分为受检异常和非受检异常。
二、🧭 理论:异常体系
2.1 继承结构
Throwable
├── Error 严重问题,程序通常不处理
└── Exception 程序可能处理的问题
├── RuntimeException 非受检异常
└── 其他 Exception 受检异常| 类型 | 常见例子 | 编译器是否强制处理 | 一般怎么做 |
|---|---|---|---|
Error | OutOfMemoryError | 否 | 排查运行环境或系统问题 |
| 受检异常 | IOException | 是 | 使用 catch 处理,或用 throws 声明 |
| 非受检异常 | NullPointerException | 否 | 优先修复代码逻辑 |
判断规则
RuntimeException及其子类、Error及其子类属于非受检异常;其余异常类属于受检异常。
2.2 三类问题一般怎么发生
2.2.1 Error:运行环境撑不住了
例如程序不断创建对象并保留引用,导致 JVM 堆内存耗尽:
List<byte[]> data = new ArrayList<>();
while (true) {
// 对象一直被 data 引用,无法被垃圾回收。
data.add(new byte[1024 * 1024]);
}最终可能抛出 OutOfMemoryError。这类问题通常不靠 catch 恢复,而要检查内存泄漏、数据量以及 JVM 内存配置。
2.2.2 受检异常:外部条件不可靠
// 文件可能不存在、没有权限,或者读取过程中发生故障。
String content = Files.readString(Path.of("demo.txt"));文件状态不完全由程序控制,因此 Files.readString() 可能抛出 IOException。调用方必须使用 catch 处理,或者使用 throws 继续向上声明,否则无法通过编译。
2.2.3 非受检异常:代码逻辑有问题
String name = null;
// null 没有指向真实对象,调用方法时会抛出 NullPointerException。
System.out.println(name.length());编译器不会强制处理 NullPointerException。这类异常通常意味着参数校验或代码逻辑存在问题,应优先修复原因,而不是简单地用 catch 忽略。
快速记忆
Error:环境撑不住;受检异常:外部情况可能失败;非受检异常:代码逻辑需要修复。
三、⚙️ 理论:异常如何传播
3.1 沿调用栈向上寻找处理者
“调用栈向上”是指:从当前出错的方法,回到调用它的方法,再回到更上一层调用者,并不是在源代码中向上寻找。
例如下面的调用关系:
main()
└── orderService()
└── queryProduct()public class ExceptionDemo {
public static void main(String[] args) {
try {
orderService();
} catch (IllegalArgumentException e) {
// queryProduct 和 orderService 都没有处理,最终由 main 处理。
System.out.println("main 处理异常:" + e.getMessage());
}
}
static void orderService() {
// 调用查询商品的方法。
queryProduct();
}
static void queryProduct() {
// 当前方法主动抛出异常,但没有在本方法中捕获。
throw new IllegalArgumentException("商品编号错误");
}
}异常的寻找过程是:
queryProduct 抛出异常
↑ 本方法没有 catch
orderService 也没有 catch
↑ 继续回到调用者
main 找到匹配的 catch
↑
异常被处理如果 main() 也没有匹配的 catch,异常就没有人处理,当前线程会结束,并打印异常堆栈。
记忆口诀
哪里出错,从哪里开始找;本层不处理,就交给上一层调用者。
3.2 多个 catch 为什么先写子类
“先匹配小异常”说的是同一个 try 后有多个 catch 时,要先写范围小的子类异常,再写范围大的父类异常:
try {
// 可能发生异常的代码。
} catch (NullPointerException e) {
// 先处理更具体的子类异常。
} catch (RuntimeException e) {
// 再处理范围更大的父类异常。
}如果先写 RuntimeException,它已经可以捕获后面的 NullPointerException,因此更具体的 catch 永远无法执行,编译器会报错。
四、🚀 实践:最小示例
4.1 可以拿来干什么
使用异常可以把正常业务流程和错误处理分开。与文件、网络等外部资源交互时,常见做法是捕获可以恢复的受检异常;参数错误等编程问题通常使用非受检异常表示。
4.2 完整实践:捕获受检异常
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
public class ExceptionDemo {
public static void main(String[] args) {
try {
// readString 可能抛出 IOException,因此必须捕获或继续声明抛出。
String content = Files.readString(Path.of("demo.txt"));
System.out.println(content);
} catch (IOException e) {
// 这里只处理能够预料到的文件读取失败。
System.out.println("文件读取失败:" + e.getMessage());
}
}
}文件存在时输出文件内容;文件不存在或无法读取时,进入 catch 并输出错误信息。
五、🎤 面试时怎么流利地说
5.1 30 秒标准回答
Java 中所有异常和错误都继承 Throwable,下面主要分为 Error 和 Exception。Error 通常是 JVM 或系统层面的严重问题,程序一般不主动处理。Exception 是程序可能处理的问题,其中 RuntimeException 及其子类属于非受检异常,编译器不强制处理;其他 Exception 通常属于受检异常,必须使用 try-catch 捕获或者通过 throws 声明。
5.2 高频追问
throw 和 throws 有什么区别?
throw用在方法体内,真正抛出一个异常对象。throws写在方法声明上,说明这个方法可能向外抛出哪些异常。
可以直接捕获 Exception 吗?
可以,但不建议不分场景地捕获。捕获范围过大会掩盖具体错误,也不利于采用有针对性的恢复方式。
六、📌 总结
- 异常体系的根类是
Throwable。 Throwable的两个主要分支是Error和Exception。- 受检异常必须
catch或throws。 RuntimeException和Error都属于非受检异常。- 异常会沿调用栈向上寻找匹配的处理者。
七、📚 官方资料
- Java Language Specification:Chapter 11. Exceptions
- Oracle Java Tutorials:The Catch or Specify Requirement
- Oracle Java Tutorials:What Is an Exception?
延伸阅读
Web 项目中的统一异常转换和响应处理,可以继续阅读 异常处理机制。