Java 的异常体系是怎样的?

2025年 阅读约 8 分钟 面试指南 · Java面试

深入解析Java异常体系,从try-catch到全局异常处理设计,分初级、中级、高级三个层次全面讲解。

一句话总结

Throwable 分两支:Error(JVM 级错误,不应捕获)和 Exception。Exception 又分受检异常(编译期强制处理,如 IOException)和非受检异常(RuntimeException,如 NPE)。最佳实践:try-with-resources 自动关资源、全局异常处理器统一处理、自定义业务异常继承 RuntimeException。

初级理解

Java 异常的顶层类是 Throwable,分为两大分支:

Error:程序无法处理的严重错误,如 OutOfMemoryError、StackOverflowError、NoClassDefFoundError。这些错误通常由 JVM 产生,程序不应该也不建议去捕获处理。

Exception:程序可以处理的异常,又分为两类:

受检异常(Checked Exception):编译时必须处理的异常(try-catch 或 throws),如 IOException、SQLException、ClassNotFoundException。如果不处理,代码无法编译通过。

非受检异常(Unchecked Exception):RuntimeException 及其子类,编译时不强制处理,如 NullPointerException、ArrayIndexOutOfBoundsException、IllegalArgumentException。通常是程序逻辑错误导致。

// 受检异常:必须处理 public void readFile() throws IOException { FileReader reader = new FileReader("test.txt"); } // 非受检异常:可以不处理 public int divide(int a, int b) { return a / b; // 可能抛出 ArithmeticException }

中级深入

try-with-resources(JDK 7):实现了 AutoCloseable 接口的资源可以自动关闭,无需在 finally 中手动关闭。

// 传统写法 BufferedReader br = null; try { br = new BufferedReader(new FileReader("test.txt")); String line = br.readLine(); } catch (IOException e) { e.printStackTrace(); } finally { if (br != null) { try { br.close(); } catch (IOException e) { } } } // try-with-resources try (BufferedReader br = new BufferedReader(new FileReader("test.txt"))) { String line = br.readLine(); } catch (IOException e) { e.printStackTrace(); } // br 自动关闭

异常链(Exception Chaining):在捕获一个异常后抛出另一个异常时,应该把原始异常作为 cause 传入,保留完整的异常链路。

try { // 数据库操作 } catch (SQLException e) { throw new BusinessException("业务处理失败", e); // 保留原始异常 }

finally 中的 return 陷阱:finally 中的 return 会覆盖 try/catch 中的 return 值和抛出的异常。

高级拓展

全局异常处理设计(Spring Boot 示例):

@RestControllerAdvice public class GlobalExceptionHandler { @ExceptionHandler(BusinessException.class) public Result handleBusiness(BusinessException e) { return Result.error(e.getCode(), e.getMessage()); } @ExceptionHandler(MethodArgumentNotValidException.class) public Result handleValidation(MethodArgumentNotValidException e) { String msg = e.getBindingResult().getFieldErrors() .stream().map(FieldError::getDefaultMessage) .collect(Collectors.joining(", ")); return Result.error(400, msg); } @ExceptionHandler(Exception.class) public Result handleUnknown(Exception e) { log.error("未知异常", e); return Result.error(500, "服务器内部错误"); } }

异常处理最佳实践:

1. 不要吞掉异常(空 catch 块),至少记录日志

2. 尽量使用具体异常类而非泛化的 Exception

3. 在合适的层级处理异常(底层抛出,上层处理)

4. 自定义业务异常继承 RuntimeException,避免受检异常的传播污染

5. 异常信息要包含足够的上下文(用户ID、订单号等),方便排查

面试加分项:能说出全局异常处理的设计方案和异常处理最佳实践,说明你有实际项目经验。

实战场景

场景:自定义业务异常体系

// 基础业务异常 public class BusinessException extends RuntimeException { private final int code; public BusinessException(int code, String message) { super(message); this.code = code; } public int getCode() { return code; } } // 具体业务异常 public class OrderNotFoundException extends BusinessException { public OrderNotFoundException(Long orderId) { super(404, "订单不存在: " + orderId); } } // 使用 if (order == null) { throw new OrderNotFoundException(orderId); // 全局异常处理器统一捕获 }

场景:异常链保留原始信息

// 反例:丢失原始异常 try { userService.create(user); } catch (SQLException e) { throw new BusinessException("创建用户失败"); // 原始异常丢失! } // 正例:保留异常链 try { userService.create(user); } catch (SQLException e) { throw new BusinessException("创建用户失败", e); // 原始异常作为 cause }

面试模拟

Q:受检异常和非受检异常的区别?什么时候用哪个?

A:受检异常编译期强制处理(try-catch 或 throws),非受检异常不强制。受检异常用于可恢复的情况(如文件不存在可重试),非受检异常用于编程错误(如 NPE、参数校验失败)。现代框架(Spring)倾向于使用非受检异常,避免异常传播污染。

Q:try-with-resources 的原理是什么?

A:实现了 AutoCloseable 接口的资源可以在 try() 中声明,编译后会自动生成 finally 块调用 close()。多个资源用分号分隔,关闭顺序与声明顺序相反。JDK 9 支持在 try 中使用已声明的 effectively final 变量。