ARTICLE DETAIL

资讯详情

深耕编程入门与网站建设的一线实战洞察。

Java 异常体系深度解析:从 Throwable 到自定义异常的最佳实践

Java 异常体系深度解析:从 Throwable 到自定义异常的最佳实践 前言本文面向正在学习 Java 的初学者以及对异常机制理解还不够系统的中级开发者。在实际开发中很多人对Exception和Error的区别、受检异常与非受检异常的使用场景、finally与try-with-resources的执行顺序等问题一直模棱两可导致代码中要么滥用try-catch要么直接throws Exception一把梭。本文将从 JDK 源码层面梳理 Java 异常体系的完整脉络并结合我在 Spring Boot 项目中封装统一业务异常的实战经验帮你把这些坑一次性填平。一、异常体系总览一切从 Throwable 说起Java 中的异常体系有一个共同的根类 ——java.lang.Throwable。只有继承自Throwable的类才能被 JVM 抛出或被catch语句捕获。它有两个直接子类Error和Exception。Throwable ├── Error错误无需程序处理 │ ├── OutOfMemoryError │ ├── StackOverflowError │ └── NoClassDefFoundError └── Exception异常程序可处理 ├── RuntimeException非受检异常 │ ├── NullPointerException │ ├── IllegalArgumentException │ ├── IndexOutOfBoundsException │ └── ArithmeticException └── 其他 Exception受检异常 ├── IOException ├── SQLException ├── FileNotFoundException └── InterruptedException1.1 Error程序不该捕获的 “致命错误”Error表示 JVM 层面的严重问题通常是资源耗尽或系统级故障程序本身无法恢复。例如OutOfMemoryError堆内存溢出StackOverflowError栈深度溢出常见于无限递归NoClassDefFoundError类加载失败对于Error我们不应该在业务代码中catch因为即使捕获了也无法做有效处理反而可能掩盖问题。正确做法是让程序终止然后通过日志和监控定位根因。1.2 Exception程序可以处理的异常Exception是我们日常开发中打交道最多的类它又分为两大类表格类型代表类编译期检查典型场景受检异常Checked ExceptionIOException、SQLException是必须try-catch或throws外部资源调用失败程序可恢复非受检异常Unchecked ExceptionNullPointerException、IllegalArgumentException否编译不强制代码逻辑缺陷应在编码阶段避免二、受检异常 vs 非受检异常一场持续多年的争论2.1 受检异常的设计初衷Java 是少数强制受检异常的主流语言。设计者认为对于可预期且可恢复的异常如网络中断、文件不存在编译器应该强制开发者处理避免遗漏。// 受检异常必须处理否则编译报错 public void readFile(String path) throws IOException { FileInputStream fis new FileInputStream(path); // ... }2.2 非受检异常的适用场景RuntimeException及其子类表示程序逻辑错误这类异常本应在编码阶段通过防御性编程避免而不是靠catch来兜底。// 非受检异常编译不强制但运行时可能崩溃 public void divide(int a, int b) { if (b 0) { throw new IllegalArgumentException(除数不能为 0); } return a / b; }2.3 我的实战观点在 Spring Boot 微服务架构中我所在的团队形成了一套约定跨服务调用、数据库操作、文件读写等外部依赖失败 → 使用受检异常或封装为业务异常参数校验、空指针、状态非法等代码逻辑问题 → 使用非受检异常尽早暴露Controller 层通过全局异常处理器RestControllerAdvice统一捕获避免每个方法都写try-catch签名注本文代码示例均来自我的个人专栏《Java 进阶实战笔记》注释中带有// Author: TechExplorer标记。三、异常处理的核心语法3.1 try-catch-finally 的执行顺序这是面试高频考点也是最容易踩坑的地方。直接看代码// Author: TechExplorer public static int testFinally() { try { System.out.println(执行 try 块); return 1; } catch (Exception e) { System.out.println(执行 catch 块); return 2; } finally { System.out.println(执行 finally 块); // 注意finally 中 return 会覆盖 try/catch 的返回值 // return 3; // 不建议这样写 } }执行结果执行 try 块 执行 finally 块 1关键结论finally块总是会执行除非 JVM 退出如System.exit()try或catch中的return会先暂存返回值等finally执行完后再返回绝对不要在finally中写return它会覆盖try/catch的返回值并吞掉异常3.2 try-with-resourcesJDK 7 引入的资源自动关闭在 JDK 7 之前处理流资源需要写繁琐的finally关闭逻辑而且还可能因为关闭时的异常覆盖原始异常。try-with-resources完美解决了这个问题// Author: TechExplorer // 传统写法JDK 6 及以前 public String readFileOld(String path) throws IOException { BufferedReader reader null; try { reader new BufferedReader(new FileReader(path)); return reader.readLine(); } finally { if (reader ! null) { reader.close(); // 这里可能抛出异常覆盖 try 中的异常 } } } // try-with-resources 写法JDK 7 public String readFileNew(String path) throws IOException { try (BufferedReader reader new BufferedReader(new FileReader(path))) { return reader.readLine(); } // 自动关闭 reader且关闭时的异常会被抑制suppressed }实现AutoCloseable接口的类都可以用在try-with-resources中包括InputStream、OutputStream、Connection、Statement等。3.3 throw 和 throws 的区别这两个关键字长得像但作用完全不同表格关键字位置作用throws方法签名上声明该方法可能抛出的异常类型交给调用者处理throw方法体内实际抛出一个异常对象// Author: TechExplorer // throws 声明告诉调用者这个方法可能抛出 IOException public void saveData(String data) throws IOException { if (data null) { // throw 实际抛出在这里制造一个异常对象 throw new IllegalArgumentException(数据不能为空); } // ... 写入文件可能抛出 IOException }四、自定义异常打造你的业务异常体系在真实项目中JDK 提供的标准异常往往不够用。比如用户余额不足、订单已过期、权限不够这些业务场景需要自定义异常来承载业务语义。4.1 自定义异常的规范写法// Author: TechExplorer // 自定义业务异常基类受检异常 public class BusinessException extends Exception { private final String errorCode; public BusinessException(String errorCode, String message) { super(message); this.errorCode errorCode; } public BusinessException(String errorCode, String message, Throwable cause) { super(message, cause); this.errorCode errorCode; } public String getErrorCode() { return errorCode; } } // 具体业务异常 public class InsufficientBalanceException extends BusinessException { private static final String ERROR_CODE BALANCE_001; public InsufficientBalanceException(String message) { super(ERROR_CODE, message); } public InsufficientBalanceException(String message, Throwable cause) { super(ERROR_CODE, message, cause); } }4.2 我在项目中的独特实践在我负责的电商订单系统中我设计了一套异常码枚举 全局异常处理器的方案// Author: TechExplorer // 异常码枚举 Getter AllArgsConstructor public enum ErrorCodeEnum { SUCCESS(0, 操作成功), PARAM_INVALID(10001, 参数校验失败), USER_NOT_LOGIN(20001, 用户未登录), BALANCE_INSUFFICIENT(30001, 账户余额不足), ORDER_NOT_EXIST(40001, 订单不存在), SYSTEM_ERROR(99999, 系统繁忙请稍后重试); private final String code; private final String message; }配合RestControllerAdvice做统一拦截// Author: TechExplorer RestControllerAdvice Slf4j public class GlobalExceptionHandler { ExceptionHandler(BusinessException.class) public ResultVoid handleBusinessException(BusinessException e) { log.warn(业务异常code{}, message{}, e.getErrorCode(), e.getMessage()); return Result.fail(e.getErrorCode(), e.getMessage()); } ExceptionHandler(Exception.class) public ResultVoid handleException(Exception e) { log.error(系统异常, e); return Result.fail(ErrorCodeEnum.SYSTEM_ERROR.getCode(), ErrorCodeEnum.SYSTEM_ERROR.getMessage()); } }这样做的好处是Controller 层代码干净不需要每个方法都try-catch异常码统一管理前后端对接时一目了然日志分级记录业务异常用warn系统异常用error便于监控告警五、异常处理的 7 条最佳实践5.1 不要捕获 Throwable 或 Error// 错误写法 try { // ... } catch (Throwable t) { // 会捕获 Error包括 OutOfMemoryError t.printStackTrace(); }5.2 不要空吞异常// 错误写法异常被静默吞掉排查问题时想死的心都有 try { // ... } catch (IOException e) { // 什么都不做 }至少要打印日志或者重新抛出包装后的异常。5.3 异常信息要具体不要只说 “出错了”// 不好 throw new RuntimeException(出错了); // 好 throw new IllegalArgumentException(用户 ID 不能为 null当前传入值 userId);5.4 优先使用标准异常不要重复造轮子JDK 已经提供了丰富的异常类能用标准的就用标准的参数非法 →IllegalArgumentException空指针 →NullPointerException或用Objects.requireNonNull()状态非法 →IllegalStateException不支持的操作 →UnsupportedOperationException5.5 不要在 finally 中使用 return前面已经讲过finally中的return会覆盖try/catch的返回值还会吞掉异常。5.6 对于可恢复的场景使用受检异常对于编程错误使用非受检异常这是《Effective Java》中的经典建议也是业界共识。5.7 异常不要用来控制业务流程异常的创建和捕获有性能开销需要填充栈轨迹而且用异常做流程控制会让代码可读性变差。// 不好用异常控制循环 try { int i 0; while (true) { list.get(i); } } catch (IndexOutOfBoundsException e) { // 循环结束 } // 好正常的循环条件 for (int i 0; i list.size(); i) { list.get(i); }六、常见面试题精选Q1NoClassDefFoundError和ClassNotFoundException有什么区别ClassNotFoundException是受检异常发生在编译时类存在但运行时找不到的场景通常由Class.forName()、ClassLoader.loadClass()触发NoClassDefFoundError是Error发生在编译时能找到但运行时类加载失败的场景比如静态初始化块抛出异常导致类加载失败Q2final、finally、finalize有什么区别final修饰符修饰类表示不可继承修饰方法表示不可重写修饰变量表示不可变finally异常处理的一部分保证代码块一定执行finalizeObject的方法在对象被垃圾回收前调用JDK 9 已标记为废弃不推荐使用Q3try-with-resources 中如果多个资源的 close 都抛异常会怎样第一个close抛出的异常会正常传播后续close抛出的异常会被抑制suppressed可以通过Throwable.getSuppressed()获取。这比传统finally写法中后面的异常覆盖前面的异常要合理得多。结语回到开头提到的问题很多开发者对异常体系的困惑本质上是没有从设计意图层面去理解。Error和Exception的划分、受检与非受检的区别、finally的执行机制背后都有明确的设计考量。掌握了这些原理你就能在项目中做出合理的异常处理决策而不是靠 “祖传代码” 和直觉。
返回列表