Skip to content

异常体系(Throwable、Error、Exception)

概述

Java 用异常(Exception)机制表示程序运行时的异常情况。所有异常类型的根类是 Throwable,其下分为 ErrorExceptionError 表示 JVM 或系统级严重错误,通常不应被捕获;Exception 表示可被程序处理的异常,又分为受检异常非受检异常。理解这套体系有助于在编码时正确选择「捕获」「声明抛出」或「不处理」。


继承关系

Throwable
├── Error                    (严重错误,一般不捕获)
│   ├── OutOfMemoryError
│   ├── StackOverflowError
│   └── ...
└── Exception                (可处理的异常)
    ├── RuntimeException     (非受检异常)
    │   ├── NullPointerException
    │   ├── IllegalArgumentException
    │   ├── IndexOutOfBoundsException
    │   └── ...
    └── 其他 Exception 子类   (受检异常,如 IOException、SQLException)
类型说明是否必须处理
ErrorJVM 或系统级错误,程序通常无法恢复一般不捕获
受检异常除 RuntimeException 及其子类外的 Exception必须 try-catch 或 throws
非受检异常RuntimeException 及其子类不强制,可按需捕获

Throwable:异常与错误的根类

Throwable 是所有可被「抛出」的类型的父类。只有 Throwable 及其子类的实例才能作为 throw 的对象,并出现在 catch 子句中。

常用方法:

  • getMessage():获取异常信息字符串
  • printStackTrace():将堆栈跟踪打印到标准错误流
  • getCause():获取导致当前异常的 cause(若存在)
java
try {
    throw new RuntimeException("出错了");
} catch (Throwable t) {
    System.out.println(t.getMessage());  // 出错了
    t.printStackTrace();                  // 打印堆栈
}

Error:不应捕获的严重错误

Error 及其子类表示 JVM 或底层资源出现的严重问题,例如内存耗尽、栈溢出、类加载失败等。这类错误通常不应在业务代码里用 try-catch 捕获,因为程序往往无法从中恢复,捕获后也难以做有意义的处理。

常见子类:

类型典型场景
OutOfMemoryError堆内存不足
StackOverflowError递归过深导致栈溢出
NoClassDefFoundError类在编译时存在、运行时找不到
java
// 仅作演示:递归导致栈溢出,抛出 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线程在等待/睡眠时被中断
java
// 受检异常必须处理: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对象状态不符合操作前提
java
// 非受检异常不强制声明或捕获
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)),便于排查根因。

相关链接

基于 VitePress 构建