← 返回文章

Java 异常体系

发布于 2026/10/19

1. 完整回答

整个异常体系的根类是 Throwable,下面主要分为 Error 和 Exception 两大类。

Error 通常表示比较严重的系统级问题,比如 OutOfMemoryError 和 StackOverflowError,一般不建议在业务代码中捕获处理。

Exception 表示应用程序可能遇到的异常,又可以分为受检异常和非受检异常。

受检异常,比如 IOException、SQLException,编译器要求必须捕获或者通过 throws 声明抛出。

非受检异常主要是 RuntimeException 及其子类,比如 NullPointerException、IllegalArgumentException,编译器不强制处理,通常和参数不合法、程序逻辑错误等问题有关。

在处理机制上,Java 通过 throw 抛出异常,通过 try-catch 捕获异常,通过 finally 执行清理逻辑。如果当前方法没有处理异常,异常就会沿着方法调用栈向上查找匹配的处理器。

另外,Java 7 引入了 try-with-resources,可以自动关闭实现了 AutoCloseable 接口的资源,减少资源泄漏。

实际开发中,异常处理一般遵循能够处理就捕获,无法处理就向上抛出的原则。比如参数校验失败可以抛出业务异常,再由全局异常处理器统一转换成接口错误响应,同时保留异常原因和日志,方便问题排查。

2. 核心原理与关键概念

2.1 Java 异常体系结构

Throwable
├── Error
│   ├── OutOfMemoryError
│   ├── StackOverflowError
│   └── LinkageError
│
└── Exception
    ├── IOException
    ├── SQLException
    ├── ClassNotFoundException
    │
    └── RuntimeException
        ├── NullPointerException
        ├── IllegalArgumentException
        ├── IndexOutOfBoundsException
        ├── ClassCastException
        └── ArithmeticException

主要分为三类:

类型 典型异常 特点
Error OutOfMemoryError、StackOverflowError 严重错误,通常难以在业务层恢复
受检异常(Checked Exception) IOException、SQLException 编译器强制捕获或声明
非受检异常(Unchecked Exception) RuntimeException 及其子类,以及 Error 编译器不强制捕获或声明

这里需要区分两个维度:

  • 继承体系:Throwable 分为 Error 和 Exception。

  • 编译检查:RuntimeException 和 Error 及其子类属于非受检异常,其余 Throwable 子类属于受检异常。

2.2 异常传播的底层机制

异常处理的核心是方法调用栈展开和异常处理器匹配。

流程如下:

异常发生 → 创建或获取异常对象 → 抛出异常 → 查找匹配的 catch → 沿调用栈向上传播 → 处理异常或交给未捕获异常处理机制

具体过程:

  1. 抛出异常:程序执行 throw,或者 JVM 在运行过程中检测到异常,例如空指针访问。

  2. 中断当前流程:异常抛出后,当前执行路径中剩余的普通语句不会继续执行。

  3. 查找处理器:先检查当前作用域是否存在能够匹配异常类型的 catch。

  4. 向上传播:如果当前方法无法处理,就退出当前方法,继续查找调用方的异常处理器。

  5. 最终处理:找到匹配的 catch 后执行处理逻辑;如果始终没有找到,就进入线程的未捕获异常处理机制。

例如:

Controller
    ↓
Service
    ↓
Repository
    ↓
抛出 SQLException
    ↓
Service 未处理,继续传播
    ↓
Controller 或统一异常处理层处理

异常向上传播时,不会重新执行出错的方法,而且栈展开过程中仍然会执行相应的 finally 清理逻辑。

2.3 异常对象包含哪些信息?

Throwable 主要保存以下信息:

信息 对应方法 作用
异常类型 getClass() 确定异常种类
异常消息 getMessage() 描述错误原因
堆栈轨迹 getStackTrace() 定位异常发生和调用的位置
原始原因 getCause() 查看引发当前异常的底层异常
被抑制异常 getSuppressed() 查看资源关闭等过程中产生的附加异常

例如,数据库访问失败后,可以封装成业务层异常,同时通过 cause 保留原始异常,方便沿着异常链排查问题。

2.4 常见异常处理方式

关键字或机制 作用
try 包围可能发生异常的代码
catch 捕获并处理匹配的异常
finally 执行清理逻辑
throw 主动抛出异常对象
throws 声明方法可能向外传播的异常
try-with-resources 自动关闭资源

在实际项目中,通常使用 try-catch 处理能够恢复的问题,使用 throws 让上层决定处理策略,使用 try-with-resources 管理可关闭资源。

3. 高频面试追问

3.1 受检异常和非受检异常有什么区别?为什么 Java 要这样设计?

核心区别是编译器是否强制处理。

受检异常要求调用方通过 try-catch 捕获,或者使用 throws 声明向上抛出,比如 IOException。

非受检异常不要求显式处理,比如 NullPointerException、IllegalArgumentException。

Java 这样设计,主要是为了区分不同的错误处理责任。

受检异常通常用于调用方有机会采取恢复措施的情况,比如文件读取失败,可以更换文件或者提示用户重试。

非受检异常通常用于程序逻辑错误、参数不合法等情况,这类问题往往需要修改代码或者由上层统一处理。

不过,受检和非受检只是编译期的分类,不代表异常一定能否恢复。实际选择时,还需要考虑调用方有没有合理的处理方式,以及异常声明会不会让接口过于复杂。

3.2 Error 和 Exception 有什么区别?Error 能被捕获吗?

Error 和 Exception 都继承自 Throwable,但表达的问题性质不同。

Error 通常代表严重的运行环境或系统级问题,比如堆内存不足导致的 OutOfMemoryError,或者线程调用栈溢出导致的 StackOverflowError。

Exception 更多表示应用程序运行过程中出现的异常,比如文件读取失败、参数不合法等。

从语法上看,Error 可以被 catch 捕获,catch(Throwable) 也能捕获它。

但业务代码通常不建议捕获这类错误,因为程序可能已经处于不可靠的状态,捕获后继续运行也未必安全。

3.3 throw 和 throws 有什么区别?

throw 用于真正抛出异常,throws 用于声明方法可能向外传播的异常。

throw 写在方法体内部,后面跟一个异常对象。例如参数不合法时,可以通过 throw new IllegalArgumentException() 主动终止当前流程。

throws 写在方法声明上,后面跟异常类型,例如 throws IOException,表示方法可能把这个异常交给调用方处理。

另外,throws 本身不会产生异常,实际是否抛出取决于方法执行过程。

对于受检异常,如果方法可能向外传播,就必须进行相应的声明;对于非受检异常,声明是可选的。

3.4 finally 一定会执行吗?try 中有 return 会怎样?

finally 通常会执行,包括 try 中发生异常或者执行 return 的情况,但不能保证所有情况下都执行。

正常情况下,执行顺序是:

try 执行 → 产生返回结果或异常 → 执行 finally → 完成返回或异常传播

例如,try 中执行 return 1,finally 中只是打印日志,那么最终仍然返回 1。

但如果 finally 中又执行了 return 2,最终返回值就会变成 2。

同样,如果 try 中抛出了异常,而 finally 又抛出新的异常,新的异常会覆盖原来的异常。

另外,调用 System.exit() 导致 JVM 退出、进程被强制终止等情况下,finally 可能来不及执行。

所以,不建议在 finally 中使用 return 或主动抛出新异常,避免覆盖原有的执行结果。

继续追问:finally 中修改变量,会影响 try 的返回值吗?

如果返回的是基本类型或者不可变引用值,return 表达式的结果已经在执行 finally 前确定,修改局部变量不会改变这个结果。

如果返回的是对象引用,finally 修改了该对象的内部状态,调用方拿到的对象可能体现这些修改。

3.5 try-with-resources 和 finally 有什么区别?为什么推荐前者?

两者都能用于资源清理,但 try-with-resources 更适合管理需要关闭的资源。

传统方式需要在 finally 中手动调用 close(),并考虑空值判断、关闭异常等问题。

try-with-resources 会自动关闭实现了 AutoCloseable 接口的资源,例如文件流、数据库连接等。

执行流程是:

初始化资源 → 执行业务代码 → 按初始化的相反顺序关闭资源 → 传播异常或正常结束

它还有一个重要特性,就是被抑制异常(Suppressed Exception)。

假设读取文件时发生异常,同时关闭文件流也发生异常,try-with-resources 会优先保留读取过程中的原始异常,把关闭时产生的异常记录为被抑制异常,可以通过 getSuppressed() 获取。

这样能够保留更多错误信息,避免资源关闭异常覆盖真正的业务异常。

3.6 多个 catch 的执行顺序是什么?为什么子类异常要放前面?

多个 catch 按照代码顺序进行匹配,匹配成功后只执行对应的处理块。

例如,FileNotFoundException 是 IOException 的子类。

如果先捕获 IOException,再捕获 FileNotFoundException,后面的分支永远不可能执行,编译器会直接报错。

因此,处理顺序应该是:

具体异常 → 父类异常 → 更通用的异常

Java 7 还支持多异常捕获,可以通过 catch (IOException | SQLException e) 合并处理逻辑相同的异常。

但多异常捕获中的异常类型不能存在父子继承关系。

实际开发中,优先捕获能够明确处理的异常类型,避免过度使用 catch(Exception),否则可能掩盖程序缺陷。

3.7 子类重写父类方法时,可以抛出更多异常吗?

子类重写方法时,不能新增或扩大父类方法不允许的受检异常范围。

例如,父类方法声明了 throws IOException,子类可以继续声明 IOException,也可以声明它的子类 FileNotFoundException,或者不声明任何异常。

但不能新增 SQLException 这样的无关受检异常,也不能扩大成 Exception。

原因是为了保证多态调用时的异常契约。

调用方按照父类方法的声明编写处理逻辑,实际运行时即使调用的是子类方法,也不应该突然出现额外的受检异常处理要求。

不过,非受检异常不受这一限制,子类可以抛出新的 RuntimeException。

3.8 实际项目中如何设计自定义异常?异常应该在哪一层处理?

自定义异常主要用于表达明确的业务失败原因,通常在能够统一处理的位置捕获。

比如订单库存不足,可以定义 InsufficientStockException,让调用方明确知道失败原因。

如果异常需要调用方强制处理,可以继承 Exception;如果希望由统一异常处理机制处理,通常继承 RuntimeException。

在典型的 Spring 项目中,可以采用这样的流程:

Service 检测业务异常 → 抛出自定义异常 → 全局异常处理器捕获 → 转换为统一错误响应

例如库存不足时,Service 抛出业务异常,Controller 层通过 @RestControllerAdvice 配合 @ExceptionHandler 返回明确的业务错误码和提示信息。

如果异常来自数据库或远程调用,封装成上层异常时,应该通过 cause 保留原始异常,方便排查。

另外,异常日志应当在合适的边界统一记录,避免每层都重复打印完整堆栈。无法处理的异常也不要简单捕获后忽略。

继续追问:为什么不建议使用异常控制正常业务流程?

因为异常适合表达异常情况,而常规分支可以通过条件判断或明确的返回结果处理。

频繁创建异常对象和收集堆栈信息存在额外开销,而且会增加控制流程的理解难度。

例如查询数据为空是正常业务结果时,可以返回空集合或者 Optional,没有必要每次都抛异常。

3.9 Spring 事务遇到异常一定会回滚吗?

不一定。Spring 声明式事务是否回滚,取决于异常类型、回滚规则,以及异常是否传播到事务拦截器。

在默认配置下,@Transactional 遇到 RuntimeException 或 Error 会触发回滚,遇到受检异常默认不会回滚。

如果希望受检异常也触发回滚,可以配置:

@Transactional(rollbackFor = Exception.class)

此外,事务方法内部如果通过 try-catch 把异常捕获后正常返回,事务拦截器通常感知不到这次异常,也就不会因为该异常触发回滚。

这时可以继续向外抛出异常,或者显式将事务标记为回滚。

实际排查事务没有回滚的问题时,通常会检查异常是否被捕获、回滚规则是否正确,以及调用是否经过 Spring 事务代理。

4. 易混淆概念对比

4.1 Error 与 Exception

对比维度 Error Exception
共同父类 Throwable Throwable
主要含义 严重的系统级错误 应用程序中的异常情况
典型示例 OutOfMemoryError IOException
编译检查 非受检 根据具体子类确定
处理策略 一般不在业务层捕获 按业务需要捕获或传播

4.2 Checked Exception 与 Unchecked Exception

对比维度 受检异常 非受检异常
编译器强制处理 是 否
典型类型 IOException、SQLException RuntimeException、Error 及其子类
是否必须声明 向外传播时必须声明 不强制
常见用途 需要调用方明确考虑的失败 程序错误、参数错误或无需强制处理的失败

4.3 throw 与 throws

对比维度 throw throws
作用 主动抛出异常 声明可能传播的异常
使用位置 方法体中 方法声明中
后面跟随 异常对象 异常类型
是否直接触发异常 是 否

4.4 finally 与 try-with-resources

对比维度 finally try-with-resources
主要用途 通用清理逻辑 自动关闭资源
资源关闭 手动调用 自动调用 close()
多资源关闭 手动控制顺序 自动逆序关闭
关闭异常 可能覆盖原异常 支持保留被抑制异常
使用场景 通用收尾处理 文件流、数据库连接等资源管理

4.5 cause 与 Suppressed Exception

对比维度 cause Suppressed Exception
含义 引起当前异常的原始原因 处理过程中附带产生的其他异常
典型场景 底层异常封装为业务异常 资源关闭时再次出现异常
获取方法 getCause() getSuppressed()
主要作用 追踪异常产生的因果关系 保留多个异常的信息

5. 总结

Java 异常体系主要用于统一处理程序运行过程中的错误,它以 Throwable 为根类,分为 Error 和 Exception,其中 Exception 又可以区分为受检异常和非受检异常。核心机制是异常抛出后沿方法调用栈向上查找匹配的处理器,由能够处理的代码负责恢复、转换或者统一返回错误信息。Java 通过 try-catch-finally 实现异常捕获和清理,也提供了 try-with-resources 自动管理资源。实际开发中,关键是合理选择异常类型、保留原始异常原因,并在合适的层次处理异常。另外,受检异常和非受检异常的区别主要体现在编译期,具体是否需要捕获、能否恢复以及事务是否回滚,还需要结合业务场景和框架规则判断。