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);
}
}
坑 3:finally 块中使用 return
finally块的代码会在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。
示例代码:
@RestControllerAdvice
public 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, “系统内部错误”);
}
}
三、总结
异常处理不是简单的 “捕获 - 打印”,而是程序健壮性设计的重要组成部分。好的异常处理应该做到:发生错误时能快速定位根因、正常业务下无额外性能损耗、代码清晰易维护。