Java 实战角度:Error 和 Exception 到底有什么区别?
这是一个 Java 面试 + 实际开发中出现频率极高的问题,但很多开发者回答得比较表面。下面从实际使用视角把两者的本质区别、内存模型中的表现、使用场景、应该怎么处理,讲得更清楚一些。
一、核心区别一句话总结
| 维度 | Error | Exception |
|---|---|---|
| 含义 | 严重系统错误,程序通常无法恢复 | 可预见、可处理的异常情况 |
| 是否必须处理 | 不需要(也不建议)捕获 | 大部分建议捕获并处理(尤其是 checked) |
| 是否可恢复 | 基本不可恢复(JVM 本身出问题) | 大部分可以恢复(业务逻辑问题) |
| 典型场景 | OutOfMemoryError、StackOverflowError、VirtualMachineError | NullPointerException、IOException、SQLException、IllegalArgumentException |
| 继承关系 | 继承自 java.lang.Error | 继承自 java.lang.Exception |
| 是否 checked | 全部 unchecked | 分 checked(必须处理)和 unchecked |
二、类继承结构图(最直观)
Throwable(顶级父类)
├── Error(严重错误)
│ ├── VirtualMachineError
│ │ ├── OutOfMemoryError
│ │ ├── StackOverflowError
│ │ └── InternalError
│ ├── LinkageError
│ └── ...其他系统级错误
└── Exception(普通异常)
├── RuntimeException(unchecked)
│ ├── NullPointerException
│ ├── ArrayIndexOutOfBoundsException
│ ├── IllegalArgumentException
│ ├── ClassCastException
│ ├── ArithmeticException
│ └── ...(绝大多数业务异常都继承这个)
└── 其他 checked Exception(必须声明或捕获)
├── IOException
│ ├── FileNotFoundException
│ └── SocketException
├── SQLException
├── ClassNotFoundException
└── ...(所有需要显式处理的异常)
三、实际开发中最常见的对比场景
| 场景 | 属于哪一类 | 你应该怎么处理?(真实项目建议) |
|---|---|---|
| 除以 0 | RuntimeException | 通常不捕获,让它抛出去,由全局异常处理器统一处理 |
| 访问 null 对象 | RuntimeException | 基本不捕获,修复代码逻辑(防御式编程 + Optional) |
| 读取配置文件不存在 | FileNotFoundException(checked) | 必须处理:要么 throws,要么 try-catch 给用户友好提示 |
| 数据库连接超时 / SQL 语法错误 | SQLException(checked) | 必须处理:回滚事务、记录日志、重试或降级 |
| JVM 堆内存不足 | OutOfMemoryError | 无法捕获,基本只能重启或扩容 |
| 递归太深导致栈溢出 | StackOverflowError | 无法合理捕获,说明代码有问题(尾递归优化、改迭代) |
| JSON 解析失败(业务输入错误) | JsonProcessingException(通常 unchecked) | 建议捕获,返回友好提示给前端 |
| 调用方传了非法参数 | IllegalArgumentException | 建议抛出,让调用方修正 |
四、实际项目中“处理”策略对比(2025–2026 主流做法)
- Error 处理原则(几乎不处理)
- 不要在业务代码里捕获
Error - 不要写
catch (Throwable t)然后把 Error 当 Exception 处理(非常危险) - 正确的做法:让它自然抛出 → 由容器(如 Spring Boot)或监控系统(Sentry、SkyWalking)捕获 → 告警 + 重启 / 扩容
- Exception 处理原则(分层处理)
- Controller 层:统一用
@ExceptionHandler或GlobalExceptionHandler转换为标准响应 - Service 层:抛出业务异常(自定义
BizException继承 RuntimeException) - Dao/Repository 层:checked 异常(如 SQLException)通常包装成 unchecked 再抛
- 工具类/底层:尽量抛出具体异常而不是泛泛的 Exception
// 推荐的业务异常定义方式(2025+ 常见写法)
public class BizException extends RuntimeException {
private final String code;
public BizException(String code, String message) {
super(message);
this.code = code;
}
public String getCode() {
return code;
}
}
// 使用示例
if (user == null) {
throw new BizException("USER_404", "用户不存在");
}
五、经典面试追问 & 标准回答
Q1:能不能 catch Error?
能,但不应该。catch Error 通常掩盖了更严重的问题,正确的做法是让它暴露出来。
Q2:为什么 RuntimeException 不需要声明 throws?
因为它是 unchecked exception,设计初衷就是“程序员的编程错误”,应该在开发阶段修复,而不是强迫调用方处理。
Q3:自定义异常应该继承 Exception 还是 RuntimeException?
- 必须强制调用方处理的 → 继承 Exception(checked)
- 属于编程错误 / 可选处理的 → 继承 RuntimeException(unchecked) ← 99% 业务异常都用这个
Q4:OutOfMemoryError 能 catch 住然后释放内存吗?
极少数情况下可以(比如 catch OutOfMemoryError 后释放一些大对象),但绝大多数场景下已经晚了,JVM 状态已经非常危险,catch 了也很难恢复。
六、总结一句话口诀(背下来答题快)
Error 是系统病危,基本等死;Exception 是感冒发烧,能治好。
Error → 不捕获,让它死得明白
Exception → 分 checked / unchecked,该捕获捕获,该抛抛
你现在是在准备面试,还是在实际项目中纠结怎么定义异常体系?
可以告诉我具体场景,我可以帮你给出更落地的异常分层方案。