Java 的异常(Exception)体系是语言最核心的工程能力之一:它决定了错误如何表达、如何传播、如何被定位与恢复。写出“可维护、可诊断、可扩展”的 Java 代码,异常设计和处理能力往往是分水岭。
本文从异常体系结构讲起,覆盖:Checked/Unchecked 的本质差异、try-catch-finally 细节、try-with-resources、异常链、常见反模式、业务异常设计、日志与统一处理等高频面试 + 实战内容。
1. 异常是什么?为什么需要异常机制
异常(Throwable)本质是:程序运行过程中出现了非预期状态,需要一种机制把“错误”从局部传递到上层,并携带足够的诊断信息(类型、堆栈、消息、原因)。
相比返回错误码,异常机制的优势:
能携带调用栈(定位成本低)
支持分类(类型系统驱动处理策略)
支持异常链(保留根因)
强制/约束处理(Checked 异常)
2. Java 异常体系总览(Throwable 树)
Java 异常顶层父类:java.lang.Throwable,主要分两大分支:
Error:严重问题,通常不可恢复(如 JVM 内存溢出、类加载错误)。一般不捕获或不做业务兜底恢复。
Exception:可处理的异常。Exception 下面再分:
Checked Exception(受检异常):必须显式处理(try-catch 或 throws)
Unchecked Exception(非受检异常):
RuntimeException及其子类,编译器不强制处理
记忆口诀:
Error 不管,Exception 要管;RuntimeException 不强制管。
3. Checked vs Unchecked:本质区别与工程取舍
3.1 Checked(受检异常)
典型:IOException,SQLException
特点:
编译器强制处理:不处理就编译不过
适合“调用方可能合理恢复”的场景:如读文件失败可以换路径/重试/降级
代价:
在层层调用中会导致“throws 传染”
容易出现大量样板代码(try-catch/throws)
3.2 Unchecked(非受检异常)
典型:NullPointerException,IllegalArgumentException,IndexOutOfBoundsException
特点:
不强制处理
更适合:编程错误、参数非法、状态不一致等“应当修代码而非恢复”的问题
3.3 实战建议(重要)
业务校验失败、用户输入错误、业务规则不满足:通常用自定义运行时异常(Unchecked)
外部依赖失败(IO、网络、DB)且调用方确实可恢复:可以保留 Checked,或在边界层转为业务异常
在服务端工程(Spring Boot)中更常见的实践是:
底层异常统一包装为业务运行时异常 + 全局异常处理(便于统一返回、统一日志、减少 throws 传染)
4. try-catch-finally 语义细节(面试高频)
4.1 finally 一定会执行吗?
一般情况下:会执行(无论是否抛异常、是否 return)。
但以下情况 finally 可能不执行:
System.exit()直接终止 JVMJVM 崩溃(如致命错误)
线程被强制杀死(极端情况)
4.2 catch 顺序:从子类到父类
否则会编译错误(父类捕获会吞掉子类分支)。
try { // ... } catch (NullPointerException e) { // 子类 } catch (RuntimeException e) { // 父类 }4.3 finally 里不要 return / throw(非常重要)
finally 里的 return 会覆盖try/catch 的返回值或异常,导致排查地狱。
public int bad() { try { return 1; } finally { return 2; // 覆盖 try 的返回 } }5. try-with-resources:资源关闭的最佳方式
JDK 7 引入,专为实现AutoCloseable的资源(流、连接、文件句柄等)设计,自动 close,且能正确处理 close 时的异常抑制(suppressed)。
try (BufferedReader br = new BufferedReader(new FileReader(path))) { return br.readLine(); } catch (IOException e) { throw new RuntimeException("读取文件失败", e); }5.1 suppressed 异常是什么?
当 try 块抛异常,close 又抛异常,close 异常会被记录为suppressed,主异常仍保留,利于排查。
Throwable[] suppressed = e.getSuppressed();6. 异常链(Cause)与“保留根因”原则
真实系统里,异常往往层层包装。最佳实践:包装异常时一定要把 cause 传进去。
正确:
catch (IOException e) { throw new BizException("文件服务不可用", e); }错误(丢失堆栈与根因):
catch (IOException e) { throw new BizException("文件服务不可用"); }定位问题时最值钱的是:原始异常类型 + 堆栈 + 触发点。
7. 自定义业务异常:高质量设计模板
7.1 为什么要自定义业务异常?
把“可预期的业务失败”与“系统故障”区分开
便于统一返回码、统一错误信息
便于全局处理与统计监控
7.2 推荐结构:错误码 + 可读消息 + 可选上下文
public class BizException extends RuntimeException { private final String code; public BizException(String code, String message) { super(message); this.code = code; } public BizException(String code, String message, Throwable cause) { super(message, cause); this.code = code; } public String getCode() { return code; } }进一步优化(可选):
code 用枚举统一管理
message 走国际化(i18n)
附带 context(如订单号、用户 id),但注意隐私与安全
8. Spring/Spring Boot 项目中的统一异常处理(常用范式)
常见做法:使用@RestControllerAdvice+@ExceptionHandler统一返回结构。
@RestControllerAdvice public class GlobalExceptionHandler { @ExceptionHandler(BizException.class) public ResponseEntity<?> handleBiz(BizException e) { // 业务异常:可控,不打 full stack 或按需打印 return ResponseEntity.badRequest().body( Map.of("code", e.getCode(), "msg", e.getMessage()) ); } @ExceptionHandler(Exception.class) public ResponseEntity<?> handleOther(Exception e) { // 系统异常:必须记录堆栈 // log.error("系统异常", e); return ResponseEntity.status(500).body( Map.of("code", "SYSTEM_ERROR", "msg", "系统繁忙,请稍后再试") ); } }工程建议:
业务异常:一般不需要打印成 ERROR 大堆栈(防止日志噪音),可按 warn/info,或打印摘要
系统异常:必须带堆栈 + traceId(链路追踪)
返回给用户的信息要“友好且不泄露内部细节”
9. 异常处理的经典反模式(高频踩坑)
9.1 catch (Exception) 然后什么都不做(吞异常)
try { doSomething(); } catch (Exception e) { // ignore }危害:问题静默,线上数据错了你都不知道。
9.2 用异常做正常流程控制
比如用try/catch判断 map 是否有 key,或用捕获 NPE 当作 if。
异常很贵(构造堆栈开销大),而且可读性差。
9.3 只打印 e.getMessage() 不打印堆栈
堆栈才是定位关键。
正确:
log.error("xxx", e);错误:
log.error(e.getMessage());
9.4 捕获后丢失 cause
见前文异常链。
9.5 finally 里做关键业务逻辑
finally 适合做资源释放、清理动作。关键逻辑放 finally 容易被覆盖/吞异常/难维护。
10. 常见运行时异常速查(附触发原因与建议)
NullPointerException:对象为空仍调用方法/字段
建议:参数校验、Optional(慎用)、合理默认值、提前失败IllegalArgumentException:方法参数非法
建议:对外接口优先抛它或 BizExceptionIllegalStateException:对象状态不对
建议:状态机/流程控制要清晰ClassCastException:类型转换错误
建议:泛型、instanceof、避免 raw typeNumberFormatException:字符串转数字失败
建议:输入校验、异常转换为业务提示IndexOutOfBoundsException:索引越界
建议:边界判断
11. 如何写出“可诊断”的异常信息(日志与消息技巧)
高质量异常信息 =发生了什么 + 为什么 + 影响什么 + 关键上下文
示例:
“下单失败”
“下单失败:库存不足,sku=xxx, need=3, left=1, orderId=xxx”
注意:
不要把敏感信息写入异常 message 和日志(如密码、token、完整身份证号)
上下文信息建议通过结构化日志字段(traceId、userId、orderId)输出
12. 一套推荐的异常分层策略(适合大多数后端项目)
按层思考:
DAO/Client 层(DB/HTTP/IO):抛出原始异常或封装成基础设施异常
Service 层:将外部异常转换为业务可理解的 BizException(保留 cause)
Controller/网关层:统一异常处理 + 返回统一格式 + 统一日志策略
目标:
业务代码不被大量 try-catch 污染
异常含义清晰、定位方便
用户返回安全、可控
13. 总结(建议收藏的要点清单)
Throwable 分 Error/Exception,Exception 分 Checked/Unchecked
受检异常强制处理,非受检异常更适合编程错误与业务失败
finally 不要 return/throw;catch 顺序从子类到父类
资源关闭优先用 try-with-resources
包装异常一定传 cause,保留根因与堆栈
业务异常建议:错误码 + message + 可选上下文
Spring 常用:@RestControllerAdvice 做统一异常处理
避免吞异常、避免用异常做流程控制、日志要打印堆栈
异常信息要可诊断,但不要泄露敏感信息