Java 的异常都有哪些?
从 Throwable 继承体系出发,讲清 Error、Exception、RuntimeException、受检异常与非受检异常的区别,并介绍常见异常、捕获规则和自定义异常。
Java 的异常都有哪些?
面试回答
Java 中所有能够被 throw 抛出或被 catch 捕获的对象都继承自 Throwable。Throwable 主要分为 Error 和 Exception 两个分支。
Error 表示 JVM 或运行环境中的严重问题,例如 OutOfMemoryError、StackOverflowError 和 NoClassDefFoundError,应用通常不应该把捕获并继续运行作为常规处理方案。Exception 表示应用可以识别和处理的异常情况,其中又分为受检异常和运行时异常。
受检异常是除 RuntimeException 及其子类之外的 Exception,例如 IOException、SQLException、ClassNotFoundException。编译器要求调用方捕获它们,或者使用 throws 继续声明。RuntimeException 及其子类属于非受检异常,例如 NullPointerException、IllegalArgumentException、IndexOutOfBoundsException,编译器不强制捕获或声明,通常表示参数、状态或程序逻辑存在问题。
一句话总结:
Throwable 分为 Error 和 Exception;Exception 中除 RuntimeException 体系外通常属于受检异常,而 RuntimeException 与 Error 体系都属于非受检异常。
一张图看懂
详细讲解
一、Java 异常体系的根类 Throwable
Java 的可抛出对象必须是 Throwable 或其子类:
Object
└─ Throwable
├─ Error
└─ Exception
└─ RuntimeException
Throwable 保存了异常处理中常用的信息:
message:异常的说明信息;cause:导致当前异常的上层原因;stackTrace:异常创建时的线程调用栈;suppressed:被抑制的其他异常,常见于try-with-resources。
只有 Throwable 及其子类实例能够用于:
throw new IllegalArgumentException("订单编号不能为空");
catch 参数也必须是 Throwable 的子类:
try {
createOrder();
} catch (IllegalArgumentException exception) {
// 处理异常
}
二、Error 表示什么
Error 表示 JVM、类加载或运行环境出现的严重问题。常见类型包括:
| 类型 | 通常表示 |
|---|---|
OutOfMemoryError | Java 堆、元空间、直接内存或本地线程等资源无法继续分配 |
StackOverflowError | 线程栈因递归过深等原因溢出 |
NoClassDefFoundError | 运行时无法使用原本期望存在的类定义 |
ExceptionInInitializerError | 类初始化方法执行失败 |
AssertionError | assert 断言失败 |
Error 也能被 catch,但“语法上能够捕获”不代表“业务上应该捕获后继续运行”。例如发生 OutOfMemoryError 后,进程可能已经没有足够资源完成复杂的恢复逻辑。常见做法是保留必要诊断信息,由进程管理或容器完成重启,并从根因上处理资源耗尽问题。
三、Exception 表示什么
Exception 表示程序执行期间能够被应用识别的异常情况。它包含两类:
Exception
├─ 受检异常
│ ├─ IOException
│ ├─ SQLException
│ ├─ ClassNotFoundException
│ └─ InterruptedException
│
└─ RuntimeException
├─ NullPointerException
├─ IllegalArgumentException
├─ IllegalStateException
└─ IndexOutOfBoundsException
这里的分类依据不是异常是否真的发生在“运行时”。所有异常最终都发生在程序运行过程中,RuntimeException 只是一个具体的继承分支。
真正的区别是:
编译器是否强制调用方捕获或声明这个异常。
四、什么是受检异常
受检异常也称 Checked Exception。对于常见业务代码,可以理解为:
继承自
Exception,但不继承自RuntimeException的异常。
例如读取文件可能抛出 IOException:
public String readConfig(Path path) throws IOException {
return Files.readString(path, StandardCharsets.UTF_8);
}
调用方必须选择一种方式。
1. 捕获并处理
try {
String config = readConfig(path);
applyConfig(config);
} catch (IOException exception) {
log.error("读取配置文件失败", exception);
useDefaultConfig();
}
2. 使用 throws 继续声明
public void initialize(Path path) throws IOException {
String config = readConfig(path);
applyConfig(config);
}
如果既不捕获也不声明,代码无法通过编译。
常见受检异常包括:
| 类型 | 常见场景 |
|---|---|
IOException | 文件或网络 I/O 失败 |
SQLException | 通过 JDBC 直接访问数据库失败 |
ClassNotFoundException | 按类名动态加载类时找不到目标类 |
InterruptedException | 阻塞线程收到中断请求 |
ParseException | 文本无法按约定格式解析 |
ReflectiveOperationException | 反射查找或调用失败的公共父类 |
受检异常通常用于表示调用方有机会处理或恢复的外部失败,但这是一种设计选择,并不意味着所有可恢复问题都必须设计成受检异常。
五、什么是 RuntimeException
RuntimeException 及其子类属于非受检异常,也称 Unchecked Exception。编译器不强制调用方捕获或在 throws 中声明。
例如:
public Order findOrder(String orderId) {
if (orderId == null || orderId.isBlank()) {
throw new IllegalArgumentException("订单编号不能为空");
}
return repository.findById(orderId)
.orElseThrow(() ->
new OrderNotFoundException(orderId));
}
常见运行时异常包括:
| 类型 | 通常表示 |
|---|---|
NullPointerException | 使用了空引用 |
IllegalArgumentException | 方法参数不符合要求 |
IllegalStateException | 对象当前状态不允许执行该操作 |
IndexOutOfBoundsException | 数组、字符串或集合下标越界 |
ClassCastException | 对象无法转换为目标类型 |
ArithmeticException | 整数除零等算术错误 |
ConcurrentModificationException | 遍历期间检测到不允许的结构性修改 |
UnsupportedOperationException | 当前实现不支持所调用的操作 |
NoSuchElementException | 请求的元素不存在 |
RejectedExecutionException | 线程池无法接受新任务 |
非受检异常不是“不需要处理”,而是编译器不强制处理。应用仍然可以在合适的边界进行统一转换、记录或返回错误响应。
六、受检异常与非受检异常的准确边界
从 Java 编译期检查规则看:
受检异常
= Throwable 的子类
- RuntimeException 体系
- Error 体系
非受检异常
= RuntimeException 及其子类
+ Error 及其子类
日常开发中通常不会直接继承 Throwable,因此经常简化成:
Exception 中除 RuntimeException 体系外的是受检异常
RuntimeException 和 Error 体系是非受检异常
| 对比项 | 受检异常 | 非受检异常 |
|---|---|---|
| 典型父类 | Exception | RuntimeException、Error |
| 编译器强制处理 | 是 | 否 |
| 是否可以捕获 | 可以 | 可以 |
| 是否可以声明 throws | 可以 | 可以,但不强制 |
| 常见含义 | 调用方可能处理的外部失败 | 程序错误、非法状态或严重运行环境问题 |
七、throw 和 throws 有什么区别
throw 用于真正抛出一个异常对象:
throw new IllegalStateException("订单已经关闭");
throws 写在方法声明中,表示该方法可能把异常继续交给调用方:
public String read(Path path) throws IOException {
return Files.readString(path);
}
可以记成:
throw → 抛出一个具体异常对象
throws → 声明方法可能向外传播哪些异常
八、异常是如何匹配 catch 的
异常抛出后,JVM 会沿调用栈向上寻找第一个类型匹配的 catch:
try {
process(input);
} catch (IllegalArgumentException exception) {
handleInvalidArgument(exception);
} catch (RuntimeException exception) {
handleRuntimeFailure(exception);
}
子类异常必须写在父类异常前面。如果先捕获父类,后面的子类分支永远无法到达,编译器会直接报错:
try {
process(input);
} catch (RuntimeException exception) {
// 已经包含 IllegalArgumentException
} catch (IllegalArgumentException exception) {
// 无法到达,不能通过编译
}
多个互不具有继承关系的异常可以使用 multi-catch:
try {
loadData();
} catch (IOException | SQLException exception) {
log.error("加载数据失败", exception);
}
九、finally 和 try-with-resources
finally 通常用于执行清理逻辑:
InputStream input = null;
try {
input = Files.newInputStream(path);
return input.read();
} finally {
if (input != null) {
input.close();
}
}
对于实现了 AutoCloseable 的资源,更推荐使用 try-with-resources:
try (InputStream input = Files.newInputStream(path)) {
return input.read();
}
它会自动关闭资源。如果业务代码和 close() 同时抛出异常,业务异常会作为主要异常继续传播,关闭资源产生的异常会记录在 getSuppressed() 中,避免清理异常覆盖真正的失败原因。
不要在 finally 中 return,因为它可能覆盖 try 中的返回结果或正在传播的异常。
十、异常链与异常转换
应用分层时,可以把底层异常转换成更符合当前业务语义的异常,但应该保留原始原因:
try {
return repository.findById(orderId);
} catch (SQLException exception) {
throw new OrderQueryException(
"查询订单失败:" + orderId,
exception
);
}
对应的自定义异常:
public class OrderQueryException extends RuntimeException {
public OrderQueryException(
String message,
Throwable cause
) {
super(message, cause);
}
}
这样日志既能表达“查询订单失败”的业务语义,也能通过 cause 保留原始数据库异常和调用栈。
错误写法是只保留新消息,丢失原始异常:
catch (SQLException exception) {
throw new OrderQueryException("查询订单失败");
}
十一、自定义异常应该继承谁
选择自定义异常父类时,可以先判断调用方是否被要求立即处理:
1. 继承 RuntimeException
public class OrderNotFoundException
extends RuntimeException {
public OrderNotFoundException(String orderId) {
super("订单不存在:" + orderId);
}
}
适合业务校验失败、资源不存在、状态冲突等通常由统一异常处理器转换为错误响应的场景。
2. 继承 Exception
public class ReportExportException
extends Exception {
public ReportExportException(
String message,
Throwable cause
) {
super(message, cause);
}
}
适合调用方确实需要显式决定重试、降级、改用备用方案或终止流程的场景。
不要只是为了避免编写 try-catch,就把所有异常都设计成 RuntimeException;也不要把每个业务失败都设计成受检异常,否则会产生大量只负责继续抛出的模板代码。
十二、InterruptedException 要特别处理
捕获 InterruptedException 后,如果当前方法不能直接继续抛出,通常应该恢复线程的中断标记:
try {
blockingQueue.take();
} catch (InterruptedException exception) {
Thread.currentThread().interrupt();
throw new TaskInterruptedException(
"任务等待被中断",
exception
);
}
如果只记录日志然后继续执行,上层代码可能无法知道线程已经收到取消或停止请求。
十三、常见错误做法
1. 捕获后什么都不做
try {
process();
} catch (Exception exception) {
}
这会吞掉失败信息,使调用方误以为操作成功。
2. 到处捕获 Exception
catch (Exception exception) {
return null;
}
过宽的捕获范围可能把编程错误、数据错误和外部系统失败混在一起,也容易让空值在后续代码中造成二次异常。
3. 重复记录同一个异常
如果每一层都记录日志后继续抛出,同一次失败可能产生多条重复堆栈。通常在真正处理异常或系统边界处完整记录一次即可。
4. 用异常控制普通业务流程
异常对象通常需要收集调用栈。对于可以通过正常条件判断表达的高频分支,不要依赖反复抛出异常完成流程跳转。
十四、常见问题速查
| 问题 | 结论 |
|---|---|
Error 是 Exception 吗? | 不是,它们都是 Throwable 的直接分支 |
| 运行时异常是否只能在运行时发现? | 名字来自继承体系,不代表其他异常不在运行时发生 |
| 非受检异常能不能捕获? | 可以,只是编译器不强制捕获 |
| 受检异常能否转换为运行时异常? | 可以,但应保留 cause,并确保符合接口语义 |
finally 一定执行吗? | 正常异常传播和方法返回时通常执行,但 JVM 强制退出、进程崩溃等情况下不能保证 |
捕获 Throwable 是否合适? | 业务代码通常不合适,因为会同时捕获普通异常和严重 Error |
评论与讨论
回复 :
留下你的想法