📝416 字⏱️3 分钟📅2025-12-16📄Java Basic#Java / Exception Handling / Best Practices / Interview / Backend
Java 异常体系详解与最佳实践
Java 异常体系详解与最佳实践
异常体系架构
Java 中的所有异常和错误都继承自 Throwable 类。理解这个层级结构是掌握异常处理的基础。
Throwable (所有错误和异常的根父类)
|
├── Error (严重错误)
│ ├── OutOfMemoryError (OOM)
│ ├── StackOverflowError (栈溢出)
│ └── ... (通常是 JVM 或系统级问题,程序无法恢复,不应捕获)
│
└── Exception (程序可处理的异常)
│
├── Checked Exception (检查型/编译时异常)
│ ├── IOException (文件/网络操作)
│ ├── SQLException (数据库操作)
│ ├── ClassNotFoundException
│ └── ... (编译器强制要求处理,要么 try-catch,要么 throws)
│
└── Unchecked Exception / RuntimeException (非检查型/运行时异常)
├── NullPointerException (NPE)
├── ArrayIndexOutOfBoundsException (数组越界)
├── ClassCastException (类型转换错误)
├── ArithmeticException (算术异常, 如 /0)
└── ... (通常由代码逻辑错误导致,编译器不强制处理)
核心概念与分类
1. Error vs Exception
- Error: 代表了 JVM 本身无法处理的严重错误(如内存耗尽、虚拟机崩溃)。程序不应该尝试捕获这些错误,因为一旦发生,程序通常无法恢复正常运行。
- Exception: 代表程序运行过程中可以预见、可以捕获并处理的异常情况。
2. Checked vs Unchecked (RuntimeException)
这是面试中最常问到的区别,可以用口诀记忆:“检查要动手,运行靠逻辑”。
| 特性 | Checked Exception (检查型) | Unchecked Exception (运行时) |
|---|---|---|
| 定义 | Exception 下除 RuntimeException 之外的异常 | RuntimeException 及其子类 |
| 强制性 | 编译器强制要求处理 | 编译器不强制要求处理 |
| 处理方式 | 必须 try-catch 捕获 或 throws 声明抛出 | 不需要显式捕获,应通过代码逻辑避免 |
| 典型场景 | 文件 IO、数据库连接、网络请求 | 空指针、数组越界、参数错误 |
关键字与语法解析
throw vs throws
throws: 用在方法签名上。- 作用: 声明该方法可能抛出的风险。告诉调用者:“我可能会出问题,你自己看着办”。
- 示例:
public void readFile() throws IOException { ... }
throw: 用在方法体内部。- 作用: 执行抛出异常对象的动作。
- 示例:
if (obj == null) throw new NullPointerException("参数为空");
try-catch-finally 执行机制
try: 标记危险区域,包裹可能抛出异常的代码。catch: 捕获处理异常。- 注意: 必须先捕获子类异常,再捕获父类异常。如果
Exception在IOException前面,具体的 IO 异常将永远无法进入其专属的 catch 块。
- 注意: 必须先捕获子类异常,再捕获父类异常。如果
finally: 保底执行。无论是否发生异常,该块代码几乎总会执行(除非 JVM 退出)。主要用于资源释放。
进阶与最佳实践
1. 资源管理的进化:try-with-resources
在 JDK 7 之前,我们需要在 finally 中繁琐地关闭流。JDK 7 引入了 try-with-resources,适用于实现了 AutoCloseable 接口的资源。
传统写法 (不推荐):
FileInputStream fis = null;
try {
fis = new FileInputStream("file.txt");
// ...
} catch (IOException e) {
e.printStackTrace();
} finally {
if (fis != null) {
try {
fis.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
现代写法 (推荐):
// 编译器自动生成关闭逻辑,安全且简洁
try (FileInputStream fis = new FileInputStream("file.txt")) {
// 业务逻辑
} catch (IOException e) {
e.printStackTrace();
}
2. 异常处理的性能与规范
- 不要用异常控制流程: 创建异常对象(需要捕获栈轨迹)开销较大。例如,不要用
try-catch来处理数组遍历结束,而应该用循环条件。 - 不要“吞”异常:
catch块中至少要记录日志。catch (Exception e) {}是大忌,这会让排查问题变得不可能。 - 异常链保持: 捕获低级异常抛出高级异常时,务必保留原始异常:
throw new BusinessException("业务错误", causeException);。
3. 自定义异常
- 目的: 提供更具业务含义的错误信息。
- 实现:
- 业务逻辑错误继承
Exception(Checked)。 - 通用技术错误继承
RuntimeException(Unchecked)。 - 必须提供包含
String message和Throwable cause的构造函数。
- 业务逻辑错误继承
高频面试题剖析
Q1: finally 块中的代码一定会执行吗?
答: 几乎一定。只有在 try 块中调用了 System.exit(0) 强制终止 JVM 时,finally 才不会执行。
Q2: 如果 try 中有 return,finally 还会执行吗?
答: 会执行。
执行顺序是:
- 执行
try中的代码。 - 遇到
return,先计算返回值并暂存。 - 执行
finally块。 - 如果
finally中没有return:方法返回第 2 步暂存的值。 - 如果
finally中也有return(危险):finally的返回值会覆盖try中的返回值。应避免在finally中写return。
Q3: 常见的运行时异常有哪些?
NullPointerException(NPE)ArrayIndexOutOfBoundsExceptionClassCastExceptionIllegalArgumentExceptionArithmeticException