跳到主要内容

异常处理

问题

Java 的异常体系是怎样的?checked 和 unchecked 异常的区别是什么?try-with-resources 怎么用?

答案

异常体系

分类说明举例
ErrorJVM 层面的严重错误,程序不应捕获StackOverflowErrorOutOfMemoryError
受检异常(Checked)编译期强制处理(try-catch 或 throws)IOExceptionSQLExceptionClassNotFoundException
非受检异常(Unchecked)RuntimeException 及其子类,编译器不强制处理NullPointerExceptionArrayIndexOutOfBoundsException

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");
}
}

异常处理最佳实践

  1. 精确捕获:捕获具体异常,不要直接 catch (Exception e)
  2. 不要吞掉异常:至少要记录日志
  3. 使用 try-with-resources 替代手动 finally 关闭资源
  4. 不要用异常控制流程:异常处理性能差,应用于真正的异常情况
  5. 保留异常链throw new BusinessException("msg", cause) 传入原始异常
  6. 早抛出,晚捕获:在底层抛出异常,在合适的层级统一处理

常见面试问题

Q1: Checked 和 Unchecked 异常的区别?

答案

维度Checked 异常Unchecked 异常
继承Exception(非 RuntimeException)RuntimeException 及子类
编译检查必须处理(try-catch 或 throws)不强制处理
典型场景IO、网络、数据库等外部操作编程错误(空指针、越界)
示例IOExceptionSQLExceptionNullPointerExceptionClassCastException
设计趋势

现代 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

执行顺序:

  1. try 中的 return 1 会先计算返回值(1),暂存
  2. 跳转到 finally 块执行
  3. 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 的区别?

答案

维度ClassNotFoundExceptionNoClassDefFoundError
类型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 级别的严重错误,程序不应该尝试捕获和处理(如 OutOfMemoryErrorStackOverflowError
  • 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 对异常处理有哪些改进?

答案

  1. 多异常捕获catch (IOException | SQLException e)
  2. try-with-resources:自动关闭资源
  3. 更精确的类型推断catch 块中重新 throw 时编译器能推断精确类型
// JDK 7+ 多异常捕获(注意 e 是隐式 final 的,不能重新赋值)
try {
// ...
} catch (IOException | SQLException e) {
logger.error("IO 或 SQL 错误", e);
}

相关链接