异常体系(Throwable、Error、Exception)
概述
Java 用异常(Exception)机制表示程序运行时的异常情况。所有异常类型的根类是 Throwable,其下分为 Error 和 Exception:Error 表示 JVM 或系统级严重错误,通常不应被捕获;Exception 表示可被程序处理的异常,又分为受检异常和非受检异常。理解这套体系有助于在编码时正确选择「捕获」「声明抛出」或「不处理」。
继承关系
Throwable
├── Error (严重错误,一般不捕获)
│ ├── OutOfMemoryError
│ ├── StackOverflowError
│ └── ...
└── Exception (可处理的异常)
├── RuntimeException (非受检异常)
│ ├── NullPointerException
│ ├── IllegalArgumentException
│ ├── IndexOutOfBoundsException
│ └── ...
└── 其他 Exception 子类 (受检异常,如 IOException、SQLException)| 类型 | 说明 | 是否必须处理 |
|---|---|---|
| Error | JVM 或系统级错误,程序通常无法恢复 | 一般不捕获 |
| 受检异常 | 除 RuntimeException 及其子类外的 Exception | 必须 try-catch 或 throws |
| 非受检异常 | RuntimeException 及其子类 | 不强制,可按需捕获 |
Throwable:异常与错误的根类
Throwable 是所有可被「抛出」的类型的父类。只有 Throwable 及其子类的实例才能作为 throw 的对象,并出现在 catch 子句中。
常用方法:
getMessage():获取异常信息字符串printStackTrace():将堆栈跟踪打印到标准错误流getCause():获取导致当前异常的 cause(若存在)
try {
throw new RuntimeException("出错了");
} catch (Throwable t) {
System.out.println(t.getMessage()); // 出错了
t.printStackTrace(); // 打印堆栈
}Error:不应捕获的严重错误
Error 及其子类表示 JVM 或底层资源出现的严重问题,例如内存耗尽、栈溢出、类加载失败等。这类错误通常不应在业务代码里用 try-catch 捕获,因为程序往往无法从中恢复,捕获后也难以做有意义的处理。
常见子类:
| 类型 | 典型场景 |
|---|---|
| OutOfMemoryError | 堆内存不足 |
| StackOverflowError | 递归过深导致栈溢出 |
| NoClassDefFoundError | 类在编译时存在、运行时找不到 |
// 仅作演示:递归导致栈溢出,抛出 StackOverflowError(Error 子类)
public static void overflow() {
overflow();
}
// 业务代码中不要试图 catch Error,应排查并修复根本原因注意
不要用 catch (Error e) 来「吞掉」严重错误。若在特殊场景(如插件容器)必须捕获 Error,应在捕获后记录并考虑重新抛出或终止进程。
Exception:可被程序处理的异常
Exception 及其子类表示程序可以且应该处理的异常,例如文件不存在、网络中断、参数不合法等。Exception 又分为两类:
受检异常(Checked Exception)
除 RuntimeException 及其子类以外的 Exception 子类都是受检异常。编译器会强制要求:要么用 try-catch 捕获,要么在方法签名上用 throws 声明抛出,否则无法通过编译。
常见受检异常:
| 类型 | 典型场景 |
|---|---|
| IOException | 文件读写、流操作失败 |
| SQLException | 数据库访问异常 |
| ClassNotFoundException | 找不到指定类(如 Class.forName) |
| InterruptedException | 线程在等待/睡眠时被中断 |
// 受检异常必须处理:try-catch 或 throws
public void readFile(String path) throws IOException {
Files.readAllLines(Path.of(path)); // 可能抛出 IOException
}
// 或在此处捕获
public void readFileSafe(String path) {
try {
Files.readAllLines(Path.of(path));
} catch (IOException e) {
System.err.println("读取失败: " + e.getMessage());
}
}非受检异常(Unchecked Exception)
RuntimeException 及其子类称为非受检异常。编译器不强制在方法上声明或捕获,是否捕获由开发者根据业务决定。常见如空指针、下标越界、非法参数等,多与编程错误或无效状态有关。
常见非受检异常:
| 类型 | 典型场景 |
|---|---|
| NullPointerException | 对 null 调用方法或访问字段 |
| IllegalArgumentException | 参数不合法 |
| IndexOutOfBoundsException | 数组/列表下标越界 |
| NumberFormatException | 字符串转数字格式错误 |
| IllegalStateException | 对象状态不符合操作前提 |
// 非受检异常不强制声明或捕获
public int parseAge(String s) {
return Integer.parseInt(s); // 若 s 格式错误,抛出 NumberFormatException
}
// 可按需在调用方捕获
public void useAge(String s) {
try {
int age = parseAge(s);
System.out.println("年龄: " + age);
} catch (NumberFormatException e) {
System.err.println("无效年龄: " + s);
}
}提示
受检异常强调「调用方必须意识到并处理这种错误」;非受检异常多表示编程错误或不可恢复状态,可由上层统一处理(如全局异常处理器)。
如何区分:何时用受检异常、何时用非受检异常
- 受检异常:可预期、可恢复或调用方必须知晓的异常(如 IO、网络、数据库)。通过强制声明或捕获,保证调用方不会忽略。
- 非受检异常:多为前置条件不满足、参数错误、空指针等,与「正确使用 API」相关。不强制处理,避免在每一层都写 throws 或 try-catch。
自定义异常时,若希望调用方必须处理,可继承 Exception(受检);若表示编程错误或不可恢复状态,可继承 RuntimeException(非受检)。详见 自定义异常。
常见误区与注意事项
不要笼统捕获 Throwable 或 Exception
在业务代码中避免 catch (Exception e) 或 catch (Throwable t) 后仅打印日志而不做任何处理,否则会吞掉本应暴露的问题。应按异常类型分别处理,或只捕获你真正能处理的类型。
受检异常不能「消失」
受检异常必须要么在方法内 try-catch,要么在方法签名上 throws,不能既不捕获也不声明。
- 打印堆栈:调试时可用
printStackTrace(),生产环境更推荐用日志框架记录完整堆栈,而不是直接打印到控制台。 - 异常链:在包装异常时,应使用带 cause 的构造方法(如
new RuntimeException("操作失败", e)),便于排查根因。