面试专题

Java 的异常都有哪些?

从 Throwable 继承体系出发,讲清 Error、Exception、RuntimeException、受检异常与非受检异常的区别,并介绍常见异常、捕获规则和自定义异常。

难度:入门更新:2026-08-03

Java 的异常都有哪些?

面试回答

Java 中所有能够被 throw 抛出或被 catch 捕获的对象都继承自 ThrowableThrowable 主要分为 ErrorException 两个分支。

Error 表示 JVM 或运行环境中的严重问题,例如 OutOfMemoryErrorStackOverflowErrorNoClassDefFoundError,应用通常不应该把捕获并继续运行作为常规处理方案。Exception 表示应用可以识别和处理的异常情况,其中又分为受检异常和运行时异常。

受检异常是除 RuntimeException 及其子类之外的 Exception,例如 IOExceptionSQLExceptionClassNotFoundException。编译器要求调用方捕获它们,或者使用 throws 继续声明。RuntimeException 及其子类属于非受检异常,例如 NullPointerExceptionIllegalArgumentExceptionIndexOutOfBoundsException,编译器不强制捕获或声明,通常表示参数、状态或程序逻辑存在问题。

一句话总结:

Throwable 分为 Error 和 Exception;Exception 中除 RuntimeException 体系外通常属于受检异常,而 RuntimeException 与 Error 体系都属于非受检异常。

一张图看懂

Java Throwable、Error、Exception、RuntimeException 与受检异常的继承关系

详细讲解

一、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、类加载或运行环境出现的严重问题。常见类型包括:

类型通常表示
OutOfMemoryErrorJava 堆、元空间、直接内存或本地线程等资源无法继续分配
StackOverflowError线程栈因递归过深等原因溢出
NoClassDefFoundError运行时无法使用原本期望存在的类定义
ExceptionInInitializerError类初始化方法执行失败
AssertionErrorassert 断言失败

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 体系是非受检异常
对比项受检异常非受检异常
典型父类ExceptionRuntimeExceptionError
编译器强制处理
是否可以捕获可以可以
是否可以声明 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() 中,避免清理异常覆盖真正的失败原因。

不要在 finallyreturn,因为它可能覆盖 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. 用异常控制普通业务流程

异常对象通常需要收集调用栈。对于可以通过正常条件判断表达的高频分支,不要依赖反复抛出异常完成流程跳转。

十四、常见问题速查

问题结论
ErrorException 吗?不是,它们都是 Throwable 的直接分支
运行时异常是否只能在运行时发现?名字来自继承体系,不代表其他异常不在运行时发生
非受检异常能不能捕获?可以,只是编译器不强制捕获
受检异常能否转换为运行时异常?可以,但应保留 cause,并确保符合接口语义
finally 一定执行吗?正常异常传播和方法返回时通常执行,但 JVM 强制退出、进程崩溃等情况下不能保证
捕获 Throwable 是否合适?业务代码通常不合适,因为会同时捕获普通异常和严重 Error

十五、参考资料

DISCUSSION

评论与讨论

留下你的想法