Java 异常处理的 8 个常见坑与最佳实践
前言在 Java 开发中异常处理是保证程序健壮性的核心环节。很多开发者对异常的认知停留在try-catch-finally的基础语法上实际编码中常常因为不规范的写法导致问题排查困难、性能损耗、资源泄漏等隐患。本文整理了 Java 异常处理中最容易踩的 8 个坑以及对应的行业通用最佳实践附完整代码示例帮你写出更健壮、更易维护的代码。一、8 个高频踩坑场景坑 1空 catch 块直接吞掉异常这是最常见也最危险的写法。捕获异常后不做任何日志记录和处理相当于直接 “吞掉” 了错误一旦线上出现问题完全无法定位根因。错误示例public void readFile(String path) {try {FileInputStream fis new FileInputStream(path);// 业务逻辑} catch (IOException e) {// 什么都不做异常直接消失}}正确做法至少打印异常栈信息生产环境建议使用日志框架记录完整上下文public void readFile(String path) {try {FileInputStream fis new FileInputStream(path);// 业务逻辑} catch (IOException e) {log.error(“读取文件失败, 文件路径:{}”, path, e);}}坑 2用 Exception 捕获所有异常不分类型直接捕获Exception甚至Throwable会把预期外的运行时异常比如空指针、数组越界也一并屏蔽掩盖代码本身的 bug。错误示例public void calculate(int a, int b) {try {int result a / b;} catch (Exception e) {log.error(“计算失败”, e);}}上述代码中如果a是null自动拆箱导致 NPE也会被统一捕获无法快速区分是参数空指针还是算术异常。正确做法捕获最具体的异常类型多个异常可以分开捕获Java 7 支持多异常并列。public void calculate(Integer a, Integer b) {try {int result a / b;} catch (ArithmeticException e) {log.error(“算术运算异常, 参数a:{}, b:{}”, a, b, e);} catch (NullPointerException e) {log.error(“参数为空, 参数a:{}, b:{}”, a, b, e);}}坑 3finally 块中使用 returnfinally块的代码会在try的 return 之前执行如果 finally 里也有 return 语句会直接覆盖 try 中的返回值导致业务逻辑错乱。错误示例public int getValue() {try {return 1;} finally {return 2; // 最终返回值会被覆盖为2}}正确做法永远不要在 finally 块中写 return 语句finally 只用于资源释放等收尾操作。坑 4丢失异常原始栈信息重新抛出异常时如果只传入错误信息而不传入原始异常对象会丢失最关键的栈追踪信息无法定位异常最初发生的位置。错误示例public void queryUser(Long id) {try {userDao.selectById(id);} catch (SQLException e) {// 只传了message丢失了原始异常栈throw new BusinessException(“查询用户失败:” e.getMessage());}}正确做法自定义异常提供支持 cause 的构造方法重新抛出时传入原始异常。public void queryUser(Long id) {try {userDao.selectById(id);} catch (SQLException e) {throw new BusinessException(“查询用户失败”, e);}}坑 5用异常做业务流程控制异常的设计初衷是处理程序非正常情况而不是用来做普通的业务逻辑判断。创建异常对象会生成栈追踪性能开销远大于普通的条件判断。错误示例public boolean isNumber(String str) {try {Integer.parseInt(str);return true;} catch (NumberFormatException e) {return false;}}正确做法使用正则、工具类等常规方式做业务判断。public boolean isNumber(String str) {if (str null || str.isEmpty()) {return false;}return str.matches(“^-?\d$”);}坑 6循环内创建并抛出异常在循环中频繁创建异常对象会因为栈追踪的生成导致严重的性能问题高并发场景下甚至会拖垮服务。错误示例for (int i 0; i 10000; i) {try {// 业务逻辑throw new RuntimeException(“循环异常”);} catch (Exception e) {// 处理}}正确做法避免在循环中抛异常可通过错误码、状态标识返回异常情况确需使用异常时可预创建异常对象关闭栈追踪慎用仅极端性能场景。坑 7资源未正确关闭在 try 块中打开 IO 流、数据库连接等资源如果不主动关闭发生异常时资源无法释放长期运行会导致资源泄漏。错误示例public void readFile(String path) throws IOException {FileInputStream fis new FileInputStream(path);// 业务逻辑如果这里抛出异常fis不会关闭byte[] buffer new byte[1024];fis.read(buffer);}正确做法Java 7 及以上优先使用try-with-resources语法自动实现资源关闭。public void readFile(String path) throws IOException {try (FileInputStream fis new FileInputStream(path)) {byte[] buffer new byte[1024];fis.read(buffer);}}坑 8自定义异常滥用很多项目里存在大量冗余的自定义异常每个业务场景都定义一个异常类导致异常体系混乱增加维护成本。正确原则按异常性质分类而不是按业务场景细分。通常项目中只需区分两大类系统异常非业务预期的异常如数据库连接失败、网络超时业务异常业务逻辑内的预期异常如参数校验不通过、库存不足二、异常处理最佳实践合理区分三类异常Java 异常体系分为Error、受检异常(Checked Exception)、非受检异常(Unchecked Exception)Error系统级错误如 OOM、栈溢出程序无法处理不要捕获受检异常编译期必须处理的异常如 IOException、SQLException可恢复场景使用非受检异常运行时异常RuntimeException 子类多为代码 bug 导致优先修复代码而非捕获异常信息携带完整上下文抛出或打印异常时务必带上关键入参、业务标识不要只输出 “操作失败” 这类无效信息。分层异常处理原则Controller 层统一捕获异常封装统一返回结果避免异常栈直接返回给前端Service 层捕获底层异常转换为业务含义明确的自定义异常补充业务上下文Dao 层不做多余捕获直接抛出原始异常交给上层处理全局统一异常处理在 SpringBoot 项目中通过RestControllerAdvice ExceptionHandler实现全局异常处理避免每个 Controller 都写重复的 try-catch。示例代码RestControllerAdvicepublic class GlobalExceptionHandler {ExceptionHandler(BusinessException.class)public Result handleBusinessException(BusinessException e) {log.warn(“业务异常:{}”, e.getMessage());return Result.fail(e.getCode(), e.getMessage());}ExceptionHandler(Exception.class)public Result handleException(Exception e) {log.error(“系统异常”, e);return Result.fail(500, “系统内部错误”);}}三、总结异常处理不是简单的 “捕获 - 打印”而是程序健壮性设计的重要组成部分。好的异常处理应该做到发生错误时能快速定位根因、正常业务下无额外性能损耗、代码清晰易维护。