Java 异常体系
(从体系结构、继承关系、底层原理、语法细节、企业坑、最佳实践、面试陷阱全覆盖)
异常核心认知
- 异常本质:程序运行时出现的非正常事件,会中断正常指令流。
- Java 异常是对象:所有异常都是
Throwable子类的实例。 - 异常处理目的:
- 避免程序直接崩溃
- 给出友好提示
- 保证资源释放
- 便于问题定位
- 异常处理不是用来控制业务逻辑的,这是企业大忌。
1. 完整异常体系结构(最顶层)
所有异常/错误都继承自 java.lang.Throwable
java.lang.Throwable
├── ① Error(错误:JVM 层面,系统级,无法处理)
└── ② Exception(异常:应用级,程序员可处理)
├── ②-1 RuntimeException(运行时异常 / 非受检异常)
└── ②-2 受检异常(编译期异常,非 RuntimeException)
2. 顶层父类:Throwable
只有 Throwable 及其子类才能被 throw、try-catch 处理
核心方法(必须掌握)
getMessage():获取异常简单描述信息getLocalizedMessage():本地化信息(一般同 getMessage)printStackTrace():打印异常堆栈(最常用)printStackTrace(PrintStream s):输出到流getStackTrace():获取堆栈跟踪数组(StackTraceElement[])initCause(Throwable cause):设置异常根源getCause():获取异常根本原因(底层异常)
3. Error(错误)
定义
- JVM 内部错误、资源耗尽、系统崩溃
- 应用程序无法捕获、无法恢复、不应该捕获
- 不需要 try-catch,也不应该 throws
常见 Error(必须认识)
VirtualMachineErrorStackOverflowError:栈溢出(递归死循环、方法调用层级过深)OutOfMemoryError:OOM 内存溢出(堆溢出、直接内存溢出)
NoClassDefFoundError:类找不到(依赖缺失、打包错误)NoSuchMethodError:方法不存在(版本冲突)LinkageError:类加载/链接错误AssertionError:断言失败
企业原则
遇到 Error 直接让程序挂掉,不要捕获,捕获也修复不了
4. Exception(异常)
分为两大类:
- RuntimeException:运行时异常(非受检)
- 受检异常(Checked Exception):编译期强制处理
4.1 受检异常 Checked Exception
特点
- 编译期强制处理:要么 try-catch,要么 throws
- 不是代码逻辑 Bug,而是外部环境不可控问题
- 继承自 Exception,但不继承 RuntimeException
常见受检异常
IOException:IO 异常(文件、网络、流)FileNotFoundExceptionEOFExceptionSocketException
SQLException:数据库异常ParseException:日期/格式解析异常ClassNotFoundException:类加载失败InterruptedException:线程中断IllegalAccessException
使用场景
底层框架、IO、数据库、网络等外部依赖操作
4.2 运行时异常 RuntimeException(非受检)
特点
- 编译期不检查,运行时才抛出
- 本质是代码逻辑 Bug
- 可处理可不处理,一般不捕获,由全局统一处理
最常见、企业高频出现的运行时异常
NullPointerException:空指针异常(NPE)→ 出现率第一IndexOutOfBoundsExceptionArrayIndexOutOfBoundsException数组越界StringIndexOutOfBoundsException字符串越界
ClassCastException:类型转换异常IllegalArgumentException:非法参数IllegalStateException:状态非法ArithmeticException:算术异常(除 0)UnsupportedOperationException:不支持的操作(如 Arrays.asList 后 add)ConcurrentModificationException:集合遍历中修改(fail-fast)NumberFormatException:数字格式转换失败UnsupportedClassVersionError:版本不兼容
5. 异常处理五大关键字详解
5.1 try
- 包裹可能抛出异常的代码
- 不能单独出现,必须配合 catch 或 finally
- try 代码块出现异常后,异常行之后的代码不再执行
try {
// 正常逻辑
// 异常发生处 → 立即跳转到匹配的 catch
// 后续代码不执行
}
5.2 catch
规则
- 可以有多个 catch
- 异常范围从小到大(子类在前,父类在后)
- 范围顺序错误会编译报错
try {
} catch (NullPointerException e) {
} catch (RuntimeException e) {
} catch (Exception e) {
}
多异常合并(JDK7+)
catch (IOException | SQLException e) {
// 处理多个异常
// 异常变量默认 final
}
5.3 finally
核心保证
无论是否异常、无论是否 return、无论是否 break,finally 一定执行
唯一不执行的情况
System.exit(0)(JVM 退出)- 线程被强制终止
- JVM 崩溃
执行顺序(面试必考)
- try 代码执行
- 遇到 return → 先保存返回值
- 执行 finally
- 真正 return
5.4 throw
手动抛出一个异常对象
if (age < 0 || age > 150) {
throw new IllegalArgumentException("年龄非法:" + age);
}
特点:
- 一旦执行 throw,方法立即结束
- 后面代码不执行
5.5 throws
方法声明异常,表示“我不处理,交给上层处理”
public void readFile() throws IOException, SQLException {
}
throw vs throws(面试必考)
- throw:方法内,手动抛一个异常对象
- throws:方法声明,声明多个异常类型
- throw 执行后方法终止;throws 只是声明
6. 异常执行流程(底层机制)
- 发生异常 → JVM 创建对应异常对象
- JVM 从当前方法开始向上回溯调用栈
- 寻找匹配的 catch 块
- 找到 → 执行 catch
- 找不到 → 线程终止,打印堆栈
- 无论如何 finally 一定执行
7. finally 三大企业致命坑(90% 开发踩过)
坑 1:finally 中 return 会覆盖 try/catch 的返回值
public static int test() {
try {
return 1;
} finally {
return 2;
}
}
// 结果:2
企业严禁在 finally 中 return
坑 2:finally 抛出异常会“吃掉”原始异常
try {
throw new RuntimeException("业务异常");
} finally {
throw new RuntimeException("释放资源异常");
}
// 原始异常完全丢失,无法排查问题
坑 3:finally 中出现死循环/阻塞,永远不退出
try { return; }
finally {
while (true) {}
}
// 方法永远不结束
8. try-with-resources(JDK7+ 企业标准)
作用
自动关闭实现 AutoCloseable 接口的资源(流、连接、锁)
语法
try (
FileInputStream fis = new FileInputStream("a.txt");
BufferedReader br = new BufferedReader(fis)
) {
// 业务代码
} catch (IOException e) {
e.printStackTrace();
}
底层原理
编译后自动生成:
- try
- catch
- finally → 调用 close()
优势
- 代码极简
- 不会丢失异常(异常被 suppressed)
- 避免忘记关流导致文件句柄泄漏、连接泄漏
- 企业开发强制使用
获取被抑制的异常
Throwable[] suppressed = e.getSuppressed();
9. 异常链(异常包裹)
底层异常抛到上层时,保留原始异常信息,避免丢失根源
try {
// ...
} catch (IOException e) {
// 把 e 作为 cause 传入
throw new BusinessException("文件读取失败", e);
}
e.getCause()可获取底层异常- 日志打印会输出完整堆栈
企业规范:所有包装异常必须传入 cause
10. 自定义异常(企业开发必备)
10.1 自定义运行时异常(推荐)
public class BusinessException extends RuntimeException {
private Integer code;
public BusinessException(String message) {
super(message);
}
public BusinessException(String message, Throwable cause) {
super(message, cause);
}
public BusinessException(Integer code, String message) {
super(message);
this.code = code;
}
}
10.2 自定义受检异常
public class CheckedBusinessException extends Exception {
// ... 同上
}
企业规范
- 业务异常全部使用 RuntimeException
- 方便全局捕获,不需要层层 throws
- 配合 SpringBoot 全局异常处理返回统一 JSON
11. 企业异常处理规范(非常重要)
- 禁止捕获 Throwable/Error
- 禁止空 catch 块(至少打日志)
- 禁止用异常做 if/else 业务判断
- 禁止在循环中频繁 try-catch(性能极差)
- 受检异常尽量底层消化,不要抛到 Controller
- 异常信息要包含:参数、场景、原因,便于排查
- 禁止在 finally 中 return / throw
- 全局统一异常处理:
@RestControllerAdvice + @ExceptionHandler - 包装异常必须传入 cause,否则无法定位根因
- 不要重复捕获、重复包装异常(堆栈混乱)
12. 运行时异常 vs 受检异常 企业选择
现代企业(SpringBoot / 微服务)
全部使用运行时异常
原因:
- 代码干净
- 无层层 throws
- 全局统一捕获
- 微服务之间异常传递更友好
受检异常适用场景
- JDK 底层 IO
- 数据库驱动
- 必须强制调用者处理的底层操作
13. 高频面试题(深度版)
- Throwable、Exception、Error 的区别?
- 受检异常和运行时异常区别?
- finally 一定执行吗?什么时候不执行?
- finally 在 return 前还是 return 后执行?
- throw 和 throws 区别?
- 异常链是什么?为什么要传 cause?
- try-with-resources 原理?
- 为什么不建议捕获 Exception/Throwable?
- 自定义异常继承 Exception 还是 RuntimeException?
- 出现 NPE 一般是什么原因?如何避免?
14. 一张图彻底记住异常体系
Throwable
├─ Error:JVM 错误,不可处理,StackOverflow / OOM
└─ Exception
├─ 受检异常:编译必须处理 → IO/SQL
└─ RuntimeException:代码Bug → NPE/越界/转换异常
处理方式:
try-catch-finally
try-with-resources(自动关流)
throw 手动抛
throws 方法声明
自定义异常 + 全局捕获