异常体系:Error、Exception、RuntimeException 一文搞懂
写 Java 代码的时候,异常处理无处不在。try-catch-throws,运行时异常,受检异常...这些概念你一定用过,但真的理解它们的区别吗?
我见过太多候选人在面试中说"Exception 是运行时异常"。还有人问"Error 和 Exception 有什么区别",答不上来。更有人不知道什么时候该 try-catch,什么时候该往外抛。
今天我们就来把这个异常体系彻底讲透。
一、真实面试场景
候选人小李在面试某大厂时,被问到这样一个问题:
"你们项目里是怎么处理异常的?能举一个例子吗?"
小李说:"我们用了 try-catch 捕获异常,然后用 log.error 输出日志。"
面试官继续追问:"那你们什么时候捕获异常,什么时候往外抛?"
小李说:"遇到异常就 try-catch 捕获,然后打印日志。"
面试官皱了皱眉:"如果遇到空指针异常呢?你们也 catch 住吗?"
小李愣了一下:"额...对,我们统一 catch 了所有异常。"
面试官摇了摇头,继续问:"那你知道 Error 和 Exception 的区别吗?RuntimeException 和普通 Exception 有什么区别?"
小张开始支支吾吾。
【面试官心理】
我想知道他有没有真正理解异常体系。很多团队的做法是"catch 所有异常然后打印日志",这种做法其实暴露了对异常体系的理解不足。RuntimeException 和普通 Exception 的处理策略完全不同,catch 所有异常会导致业务错误被吞掉,程序可能在错误状态下继续运行。
二、异常体系全景图
Java 的异常体系是一个树形结构:
Throwable
├── Error(错误)
│ ├── VirtualMachineError
│ │ ├── OutOfMemoryError
│ │ └── StackOverflowError
│ ├── ThreadDeath
│ └── LinkageError
│
└── Exception(异常)
├── IOException(受检异常)
│ ├── FileNotFoundException
│ └── SocketException
├── SQLException(受检异常)
└── RuntimeException(运行时异常)
├── NullPointerException
├── ArrayIndexOutOfBoundsException
├── ClassCastException
├── IllegalArgumentException
└── ArithmeticException
2.1 Throwable:所有异常的老祖宗
Throwable 是 Java 中所有错误和异常的父类。它有两个直接子类:
- Error:表示 JVM 无法处理的严重问题
- Exception:表示程序可以处理的异常情况
public class Throwable {
private String detailMessage;
private StackTraceElement[] stackTrace;
private Throwable cause; // 原因链
public Throwable() {}
public Throwable(String message) {}
public Throwable(String message, Throwable cause) {}
public String getMessage() {}
public void printStackTrace() {}
}
三、Error:JVM 的"不可抗力"
3.1 Error 的本质
Error 代表的是 JVM 层面的错误,是程序无法处理的。比如:
- OutOfMemoryError:内存耗尽
- StackOverflowError:栈溢出
- VirtualMachineError:JVM 崩溃
这些错误的发生不是因为你的代码有 bug,而是因为运行环境出了问题。
3.2 ❌ 错误示范:catch Error
try {
// 可能会触发 StackOverflowError 的代码
} catch (Error e) {
// 不要这样做!
System.out.println("捕获了 Error");
}
捕获 Error 是不合理的行为,因为:
- 你无法恢复:内存耗尽了,你能做什么?重启 JVM 吗?
- 程序状态不可信:JVM 已经出问题了,程序可能处于未定义状态
- 掩盖真正问题:捕获 Error 会让问题更难排查
3.3 正确做法:不要捕获 Error
对于 Error,正确的处理方式是让它传播出去,或者配置 JVM 参数:
// 方案1:让 Error 传播出去
public void readFile() throws IOException {
// 如果发生 OutOfMemoryError,让它自然传播
// 不要 catch
}
// 方案2:配置 JVM 参数应对
// -Xmx512m 设置最大堆内存
// -Xss1m 设置栈大小
⚠️
捕获 Error 是一种反模式。如果你的代码 catch 了 Error,那说明你的异常处理策略有问题。Error 是给 JVM 和 JVM 工具看的,不是给业务代码看的。
四、Exception:程序可以处理的异常
4.1 Exception 的分类
Exception 分为两类:
- 受检异常(Checked Exception):编译器要求必须处理的异常
- 运行时异常(Unchecked Exception / RuntimeException):编译器不要求强制处理的异常
Exception
├── 受检异常(Checked)—— 必须 try-catch 或 throws
│ ├── IOException
│ ├── SQLException
│ └── ClassNotFoundException
│
└── RuntimeException(Unchecked)—— 可选处理
├── NullPointerException
├── ArrayIndexOutOfBoundsException
├── ClassCastException
└── IllegalArgumentException
4.2 受检异常(Checked Exception)
受检异常是编译器强制要求处理的异常。如果方法可能抛出受检异常,必须:
- 使用
try-catch 捕获处理
- 或者使用
throws 声明抛出
public class FileService {
// 这个方法可能抛出受检异常,必须声明或捕获
public String readFile(String path) throws IOException {
BufferedReader reader = new BufferedReader(new FileReader(path));
return reader.readLine();
}
}
调用方必须处理:
public void process() {
try {
String content = fileService.readFile("config.txt");
} catch (IOException e) {
// 必须处理:要么 catch,要么继续 throws
log.error("读取文件失败", e);
}
}
4.3 运行时异常(RuntimeException)
运行时异常是编译器不强制要求处理的异常。它的出现通常表示程序有 bug:
public class UserService {
public User getUserById(Long id) {
// 如果 id 为 null,这里会抛 NullPointerException
// 但编译器不强制要求处理
return userDao.findById(id); // id 可能为空
}
}
调用方可以不处理:
public void process() {
// 可以不 catch,因为 RuntimeException 编译器不强制要求
User user = userService.getUserById(userId);
// 但如果不处理,异常会往上传播,直到被捕获或导致程序崩溃
}
4.4 【直观类比】受检异常 vs 运行时异常
想象你去餐厅吃饭:
五、常见异常详解
5.1 运行时异常(RuntimeException)
NullPointerException:空指针异常
String str = null;
str.length(); // NullPointerException
ArrayIndexOutOfBoundsException:数组越界
int[] arr = {1, 2, 3};
int x = arr[5]; // ArrayIndexOutOfBoundsException
ClassCastException:类型转换错误
Object obj = "hello";
Integer num = (Integer) obj; // ClassCastException
IllegalArgumentException:非法参数
public void setAge(int age) {
if (age < 0) {
throw new IllegalArgumentException("年龄不能为负数");
}
}
5.2 受检异常(Checked Exception)
IOException:输入输出异常
try {
FileInputStream fis = new FileInputStream("test.txt");
} catch (FileNotFoundException e) { // IOException 的子类
e.printStackTrace();
}
SQLException:数据库异常
try {
Connection conn = DriverManager.getConnection(url, user, pwd);
} catch (SQLException e) {
log.error("数据库连接失败", e);
}
ClassNotFoundException:类找不到
try {
Class.forName("com.mysql.jdbc.Driver");
} catch (ClassNotFoundException e) {
log.error("驱动类未找到", e);
}
六、生产场景与异常处理
6.1 ❌ 错误示范:捕获所有异常
try {
processOrder(orderId);
} catch (Exception e) {
log.error("处理订单失败", e); // 捕获了所有异常,包括 RuntimeException
}
问题:这种写法会吞掉 NullPointerException、IllegalArgumentException 等运行时异常,导致程序在错误状态下继续运行。
6.2 ✅ 正确做法:区分异常类型
public void processOrder(String orderId) {
// 业务校验异常,直接抛运行时异常
if (orderId == null || orderId.isEmpty()) {
throw new IllegalArgumentException("订单号不能为空");
}
try {
orderService.submit(orderId);
} catch (IOException e) {
// 受检异常,必须处理:记录日志,可以重试或回滚
log.error("提交订单失败", e);
throw new OrderSubmitException("订单提交失败", e);
}
}
6.3 异常链:保留根本原因
try {
orderDao.save(order);
} catch (SQLException e) {
// 保留原始异常作为 cause
throw new ServiceException("保存订单失败", e);
}
这样堆栈信息会完整保留:
ServiceException: 保存订单失败
at OrderService.save(OrderService.java:25)
...
Caused by: java.sql.SQLException: 数据插违反约束
at OrderDao.save(OrderDao.java:18)
...
6.4 异常抛出策略
七、面试追问链
第一层:基础概念
面试官问:"Error 和 Exception 的区别是什么?"
标准回答:Error 表示 JVM 无法处理的严重问题(如 OutOfMemoryError),程序不应该捕获 Error;Exception 表示程序可以处理的异常情况,分为受检异常(编译器要求处理)和运行时异常(不强制要求处理)。
第二层:追问受检/运行时异常
面试官追问:"受检异常和运行时异常有什么区别?"
标准回答:受检异常是编译器强制要求处理的异常(如 IOException、SQLException),如果不处理或不声明,编译不通过;运行时异常(RuntimeException)编译器不强制要求处理,通常表示程序 bug(如 NullPointerException),可以处理也可以不处理。
第三层:实际应用
面试官追问:"你们项目里怎么处理异常?什么时候捕获,什么时候抛出?"
需要结合实际场景:业务校验失败抛运行时异常,外部依赖失败捕获并转换为业务异常,重复操作需要幂等处理等等。
第四层:异常设计
面试官追问:"如果要设计一个统一的异常处理,应该怎么做?"
可以回答:定义业务异常基类,封装错误码和错误信息,全局异常处理器统一处理,记录日志,返回统一格式的错误响应。
【面试官心理】
我问他异常处理,其实是在考察他对异常体系的理解深度。知道 Error/Exception/RuntimeException 区别只是基础,能说出"业务异常应该怎么设计"、"异常链怎么保留"才是加分项。
【学习小结】
- Error:JVM 错误,程序不应捕获,让其传播
- Exception:程序可以处理的异常
- 受检异常(Checked):编译器强制处理
- 运行时异常(RuntimeException):不强制处理,通常是 bug
- 不要 catch 所有异常,会吞掉运行时异常
- 保留异常链(cause)以便排查问题
- 业务异常用运行时异常更灵活