Java异常处理
Java 异常体系以 Throwable 为根,分 Error(JVM 级错误,不处理)和 Exception(程序可处理),Exception 再分受检(Checked)和非受检(Unchecked)两类。受检异常强制编译器检查"要么捕获要么声明",这是 Java 与 C/Python 的显著差异——它把错误处理从"可选项"变成"强制项",代价是代码冗余(泛滥的 try-catch)。
异常体系
| 类别 | 代表 | 编译期强制 | 处理方式 |
|---|---|---|---|
| Error | OutOfMemoryError、StackOverflowError | 否 | 不捕获,让程序终止 |
| 受检异常 | IOException、SQLException | 是 | 必须 try-catch 或 throws |
| 非受检异常 | NPE、IllegalArgumentException | 否 | 可选,通常靠代码逻辑避免 |
判空从源头解决 NPE
非受检异常中最常见的是 NPE。防御手段不是到处 try-catch,而是前置判空、Optional(见 Java8新特性)、或使用 Objects.requireNonNull()。
public void setName(String name) {
this.name = Objects.requireNonNull(name, "name 不能为空");
}try-catch-finally 与 try-with-resources
finally 保证"无论是否抛异常都执行",用于释放资源;但资源类(流、连接)推荐用 try-with-resources,它自动调用 close(),比 finally 更简洁安全:
// 传统写法:finally 手动关闭
FileInputStream in = null;
try {
in = new FileInputStream("a.txt");
// 读取
} catch (IOException e) {
log.error("读取失败", e);
} finally {
if (in != null) {
try { in.close(); } catch (IOException ignored) { }
}
}
// try-with-resources:自动关闭,推荐
try (FileInputStream in = new FileInputStream("a.txt")) {
// 读取
} catch (IOException e) {
log.error("读取失败", e);
}finally 中禁止 return
finally 块中 return 会覆盖 try/catch 中的返回值或异常,吞掉正常逻辑:
try { return 1; }
finally { return 2; } // ❌ 永远返回 2异常链与自定义异常
异常链保留原始异常,排查根因时不可或缺:
try {
// 数据库操作
} catch (SQLException e) {
throw new BusinessException("订单保存失败", e); // 第二个参数是 cause
}自定义异常规范:继承 RuntimeException(非受检)避免侵入业务方法签名,命名以 Exception 结尾,提供 message 和 cause 构造:
public class BusinessException extends RuntimeException {
public BusinessException(String message) { super(message); }
public BusinessException(String message, Throwable cause) { super(message, cause); }
}空值处理
判空前先区分两种情况:null 是有效返回值(查询无结果)还是无效返回值(参数错误),处理策略截然不同。
null 无意义 → 抛异常:
if (param == null) {
throw new IllegalArgumentException("参数不能为空");
}null 有意义 → 返回空替代值:
return Collections.emptyList(); // 调用方可直接遍历,无需判空空对象模式(Null Object Pattern)——返回"什么都不做"的对象替代 null,调用方无需判空即可继续链式调用:
parser.findAction(someInput).doSomething(); // 精简写法,无 NPE 风险equals 判空——常量在前:
"bar".equals(foo) // 正确
foo.equals("bar") // 可能 NPEOptional 容器(Java 8+,详见 Java8新特性):
Optional<String> name = Optional.ofNullable(getName());
name.ifPresent(n -> System.out.println(n));最佳实践
| 规则 | 说明 |
|---|---|
| 不要吞异常 | catch (Exception e) { } 是生产事故源头,至少打日志 |
| 不要用异常控制流程 | parseInt 前先检查格式,别靠 NumberFormatException 判断 |
| 精确捕获 | 捕获具体异常类型,不要大而全的 catch (Exception) |
| 早抛晚接 | 底层抛具体异常,顶层统一处理并记录上下文 |
| 日志含上下文 | 打日志带业务 ID、参数,否则线上无法定位 |
日志规范
| 级别 | 用途 |
|---|---|
| ERROR | 影响功能的错误 |
| WARN | 不影响运行的警告 |
| INFO | 正常流程信息 |
| DEBUG | 开发调试 |
占位符而非拼接
日志必须用占位符,禁止字符串拼接——拼接即使不输出也会执行,浪费性能:
log.debug("用户登录: userId={}", userId); // 正确:占位符
log.debug("用户登录: " + userId); // 错误:字符串拼接