异常处理
问题
Java 的异常体系是怎样的?checked 和 unchecked 异常的区别是什么?try-with-resources 怎么用?
答案
异常体系
| 分类 | 说明 | 举例 |
|---|---|---|
| Error | JVM 层面的严重错误,程序不应捕获 | StackOverflowError、OutOfMemoryError |
| 受检异常(Checked) | 编译期强制处理(try-catch 或 throws) | IOException、SQLException、ClassNotFoundException |
| 非受检异常(Unchecked) | RuntimeException 及其子类,编译器不强制处理 | NullPointerException、ArrayIndexOutOfBoundsException |
try-catch-finally
ExceptionDemo.java
try {
// 可能抛出异常的代码
int result = 10 / 0;
} catch (ArithmeticException e) {
// 捕获特定异常
System.out.println("除零错误: " + e.getMessage());
} catch (Exception e) {
// 捕获更通用的异常(从上到下,先捕获子类再捕获父类)
System.out.println("其他错误: " + e.getMessage());
} finally {
// 无论是否异常都会执行(释放资源)
System.out.println("finally 块执行");
}
finally 的注意事项
finally中避免使用return,它会覆盖 try/catch 中的return值- 如果 JVM 退出(
System.exit())或线程被中断,finally可能不执行 finally主要用于资源释放,JDK 7+ 推荐用try-with-resources替代
try-with-resources(JDK 7+)
自动关闭实现了 AutoCloseable 接口的资源:
TryWithResources.java
// JDK 7+:资源声明在 try() 中,自动关闭
try (
FileInputStream fis = new FileInputStream("input.txt");
FileOutputStream fos = new FileOutputStream("output.txt")
) {
byte[] buffer = new byte[1024];
int len;
while ((len = fis.read(buffer)) != -1) {
fos.write(buffer, 0, len);
}
} catch (IOException e) {
e.printStackTrace();
}
// 无需 finally,fis 和 fos 会自动关闭(按声明的逆序)
// JDK 9+:可以引用外部的 effectively final 变量
FileInputStream fis = new FileInputStream("input.txt");
try (fis) { // 直接引用,无需再次声明
// ...
}
底层原理:编译器自动生成 finally 块调用 close() 方法,并通过 Suppressed Exceptions 机制处理关闭时的异常。
自定义异常
BusinessException.java
// 受检异常:继承 Exception
public class BusinessException extends Exception {
private final int code;
public BusinessException(int code, String message) {
super(message);
this.code = code;
}
public BusinessException(int code, String message, Throwable cause) {
super(message, cause);
this.code = code;
}
public int getCode() { return code; }
}
// 非受检异常:继承 RuntimeException
public class NotFoundException extends RuntimeException {
public NotFoundException(String resource) {
super(resource + " not found");
}
}
异常处理最佳实践
- 精确捕获:捕获具体异常,不要直接
catch (Exception e) - 不要吞掉异常:至少要记录日志
- 使用 try-with-resources 替代手动
finally关闭资源 - 不要用异常控制流程:异常处理性能差,应用于真正的异常情况
- 保留异常链:
throw new BusinessException("msg", cause)传入原始异常 - 早抛出,晚捕获:在底层抛出异常,在合适的层级统一处理
常见面试问题
Q1: Checked 和 Unchecked 异常的区别?
答案:
| 维度 | Checked 异常 | Unchecked 异常 |
|---|---|---|
| 继承 | Exception(非 RuntimeException) | RuntimeException 及子类 |
| 编译检查 | 必须处理(try-catch 或 throws) | 不强制处理 |
| 典型场景 | IO、网络、数据库等外部操作 | 编程错误(空指针、越界) |
| 示例 | IOException、SQLException | NullPointerException、ClassCastException |
设计趋势
现代 Java 框架(Spring、Hibernate)趋向使用 Unchecked 异常,因为 Checked 异常会污染方法签名,增加代码冗余。Spring 将 SQLException 包装为 DataAccessException(Unchecked)。
Q2: throw 和 throws 的区别?
答案:
throw:在方法体内抛出一个异常实例,如throw new IOException("文件不存在")throws:在方法签名上声明该方法可能抛出的异常,如public void read() throws IOException
public void readFile(String path) throws IOException { // throws 声明
if (path == null) {
throw new IllegalArgumentException("路径不能为空"); // throw 抛出
}
// ...
}
Q3: try-catch-finally 中 return 的执行顺序?
答案:
public int test() {
try {
return 1;
} catch (Exception e) {
return 2;
} finally {
return 3; // finally 中的 return 会覆盖 try/catch 的 return
}
}
// 结果:返回 3
执行顺序:
try中的return 1会先计算返回值(1),暂存- 跳转到
finally块执行 finally中有return 3,直接返回 3,覆盖了之前暂存的值
如果 finally 中没有 return:
public int test() {
int x = 0;
try {
x = 1;
return x; // 暂存返回值 1
} finally {
x = 2; // 修改 x,但不影响已暂存的返回值
}
}
// 结果:返回 1(基本类型的值已经被暂存)
Q4: NoClassDefFoundError 和 ClassNotFoundException 的区别?
答案:
| 维度 | ClassNotFoundException | NoClassDefFoundError |
|---|---|---|
| 类型 | Exception(受检异常) | Error |
| 触发时机 | 运行时通过反射加载类失败 | 编译时存在但运行时找不到类 |
| 典型原因 | Class.forName() 类名错误 | 依赖 jar 缺失、类初始化失败 |
| 可恢复性 | 可恢复 | 通常不可恢复 |
Q5: 异常对性能有什么影响?
答案:
异常创建和抛出的性能开销较大,主要在于填充栈轨迹(fillInStackTrace())。在高频调用路径中应避免用异常控制流程:
// ❌ 反例:用异常控制流程
try {
int value = Integer.parseInt(str);
} catch (NumberFormatException e) {
value = defaultValue;
}
// ✅ 正例:先校验
if (str != null && str.matches("-?\\d+")) {
value = Integer.parseInt(str);
} else {
value = defaultValue;
}
如果确实需要高频抛出异常(如自定义业务异常),可以重写 fillInStackTrace() 返回 this 以跳过栈轨迹填充。
Q6: Error 和 Exception 的区别?
答案:
- Error:JVM 级别的严重错误,程序不应该尝试捕获和处理(如
OutOfMemoryError、StackOverflowError) - Exception:程序级别的异常,应该被捕获或声明处理
但在某些特殊场景中,捕获 Error 也是合理的(如 Spring 捕获 NoClassDefFoundError 做条件装配),不过这属于框架级别的行为。
Q7: 什么是异常链?为什么重要?
答案:
异常链是将底层异常包装在高层异常中传递的机制,保留完整的调用信息:
public User findUser(Long id) {
try {
return userDao.selectById(id);
} catch (SQLException e) {
// 保留原始异常作为 cause
throw new ServiceException("查询用户失败", e);
}
}
如果不传入 cause,原始异常信息会丢失,排查问题时只能看到 ServiceException,看不到底层的 SQLException。
Q8: JDK 7 对异常处理有哪些改进?
答案:
- 多异常捕获:
catch (IOException | SQLException e) - try-with-resources:自动关闭资源
- 更精确的类型推断:
catch块中重新throw时编译器能推断精确类型
// JDK 7+ 多异常捕获(注意 e 是隐式 final 的,不能重新赋值)
try {
// ...
} catch (IOException | SQLException e) {
logger.error("IO 或 SQL 错误", e);
}