
目录
[1. 三大核心类](#1. 三大核心类)
[运行时异常(RuntimeException)------ 代码逻辑问题](#运行时异常(RuntimeException)—— 代码逻辑问题)
[编译时异常(Checked)------ 外部环境问题](#编译时异常(Checked)—— 外部环境问题)
[三、如何通过异常提示快速定位 Bug](#三、如何通过异常提示快速定位 Bug)
[1. 异常信息的结构解析](#1. 异常信息的结构解析)
[2. 定位 Bug 的三步法](#2. 定位 Bug 的三步法)
[3. 实战技巧](#3. 实战技巧)
二、OutOfMemoryError(堆溢出)------ "仓库爆仓"
[代码示例 1:无限创建对象](#代码示例 1:无限创建对象)
[代码示例 2:内存泄漏(无意识持有引用)](#代码示例 2:内存泄漏(无意识持有引用))
三、StackOverflowError(栈溢出)------ "俄罗斯套娃"
[代码示例 1:无限递归](#代码示例 1:无限递归)
[代码示例 2:循环依赖调用](#代码示例 2:循环依赖调用)
[三、编译时异常(Checked Exception)](#三、编译时异常(Checked Exception))
Java 异常体系是"倒树形"结构 ,所有异常都继承自 Throwable。掌握这个体系和异常信息的阅读技巧,是快速定位 Bug 的关键能力。
一、体系结构(三层金字塔)
html
Throwable(根类)
/ \
Error Exception(程序可处理)
(系统级) / \
运行时异常 编译时异常(Checked)
(RuntimeException) IOException/SQLException等
/ | \
NullPointer ArrayIndex ClassCast Arithmetic
1. 三大核心类
|-----------------------|--------------|------------------------------------|--------------------------------------------------------------------|
| 类型 | 继承关系 | 特点 | 示例 |
| Error | 继承 Throwable | 系统级错误,程序不应捕获 | OutOfMemoryError(内存溢出)、StackOverflowError(栈溢出) |
| RuntimeException | 继承 Exception | 运行时异常(Unchecked),代码逻辑错误 | NullPointerException(空指针)、ArrayIndexOutOfBoundsException(数组越界) |
| Checked Exception | 继承 Exception | 编译时异常,必须处理(try-catch 或 throws) | IOException(IO错误)、SQLException(数据库错误) |
二、常见异常速查表(面试/生产高频)
运行时异常(RuntimeException)------ 代码逻辑问题
java
// 1. NullPointerException(NPE)------ 头号杀手
String s = null;
s.length(); // 报错:Cannot invoke "String.length()" because "s" is null
// 2. ArrayIndexOutOfBoundsException------ 数组越界
int[] arr = {1,2,3};
int x = arr[5]; // 报错:Index 5 out of bounds for length 3
// 3. ClassCastException------ 类型强转错误
Object o = "hello";
Integer i = (Integer)o; // 报错:String cannot be cast to Integer
// 4. NumberFormatException------ 数字格式错误(继承 IllegalArgumentException)
int n = Integer.parseInt("abc"); // 报错:For input string: "abc"
// 5. ArithmeticException------ 算术错误
int x = 10 / 0; // 报错:/ by zero
// 6. IllegalArgumentException------ 非法参数(方法入参校验)
Thread.sleep(-1000); // 报错:timeout value is negative
编译时异常(Checked)------ 外部环境问题
java
// 1. IOException------ 文件/网络操作失败
FileReader fr = new FileReader("不存在的文件.txt"); // 编译报错,必须 try-catch
// 2. FileNotFoundException------ 文件不存在(IOException 子类)
// 3. SQLException------ 数据库操作失败
// 4. ClassNotFoundException------ 类加载失败(反射时常见)
Class.forName("com.mysql.jdbc.Driver"); // 旧版 JDBC 驱动类名写错会抛此异常
三、如何通过异常提示快速定位 Bug
1. 异常信息的结构解析
典型异常堆栈:
java
Exception in thread "main" java.lang.NullPointerException:
Cannot invoke "String.length()" because "s" is null
at com.example.Test.methodA(Test.java:15)
at com.example.Test.main(Test.java:5)
拆解:
-
java.lang.NullPointerException:异常类型(空指针) -
Cannot invoke... because "s" is null:具体原因(Java 14+ 会提示哪个变量为 null) -
at com.example.Test.methodA(Test.java:15):最关键! 出问题的类、方法、行号 15 -
at com.example.Test.main(Test.java:5):调用链,main 方法的第 5 行调用了 methodA
2. 定位 Bug 的三步法
第一步:看异常类型
-
NullPointerException→ 找哪个对象为 null -
ArrayIndexOutOfBoundsException→ 检查数组长度和索引 -
ClassCastException→ 检查强转前的类型判断(instanceof)
第二步:看堆栈顶部的 Caused By
java
// 多层嵌套异常,重点看最底层的 Caused By
try {
// ...
} catch (Exception e) {
throw new RuntimeException("业务错误", e); // 包装异常
}
日志输出:
java
java.lang.RuntimeException: 业务错误
at ...
Caused by: java.sql.SQLException: Connection refused
at ...
真相:实际是数据库连接失败,不是业务逻辑问题!
第三步:看行号和调用链
-
第一行堆栈 (最靠近
at的):直接出错位置 -
最后一行堆栈:程序入口(main 方法或线程 run 方法)
3. 实战技巧
技巧 1:利用异常信息快速修复
java
// 异常说:String index out of range: -1
String s = "hello";
char c = s.charAt(-1); // 一看就知道索引传成了负数,检查索引计算逻辑
// 异常说:For input string: "12.34"(用 parseInt 解析小数)
int x = Integer.parseInt("12.34"); // 一看就知道要用 parseDouble 或先截取
技巧 2:在 IDEA 中点击链接 现代 IDE(IntelliJ IDEA、Eclipse)会在控制台把异常堆栈中的类名和行号变成蓝色超链接 ,直接点击跳转到出错代码行。
技巧 3:自定义异常保留现场
java
public void transferMoney(Account from, Account to, BigDecimal amount) {
if (from.getBalance().compareTo(amount) < 0) {
// 抛出异常时带上关键参数,方便排查
throw new InsufficientFundsException(
"余额不足,账户:" + from.getId() +
",余额:" + from.getBalance() +
",需支付:" + amount
);
}
}
四、防御性编程建议(减少异常)
|----------------------------------|----------------------------------------------------------------------|
| 异常类型 | 预防措施 |
| NullPointerException | 使用 Objects.requireNonNull() 或 Optional,或提前 if (obj != null) 判断 |
| ArrayIndexOutOfBoundsException | 循环前检查 i < array.length,使用 for-each 循环 |
| NumberFormatException | 用正则预校验 str.matches("\\d+"),或用异常捕获兜底 |
| ClassCastException | 强转前用 instanceof 判断:if (obj instanceof String) |
| ArithmeticException | 除法前判断 divisor != 0 |
五、一句话总结
异常体系记三层:Error(系统崩)、RuntimeException(代码烂)、Checked(环境差)。看 Bug 先看堆栈第一行的类名:行号**,那是案发现场;再看异常类型,确定排查方向;最后看 Caused By,找到根本原因。**
核心区别 :OOM(OutOfMemoryError)是"东西太多装不下"(堆满了),StackOverflowError 是"套娃太深卡住了"(栈撑爆了)。
一、对比速查表
|----------|---------------------------------|-----------------------------|
| 维度 | OutOfMemoryError(内存溢出) | StackOverflowError(栈溢出) |
| 发生位置 | 堆内存(Heap)或 方法区(Metaspace) | 虚拟机栈(Stack) |
| 根本原因 | 创建了太多对象,垃圾回收器跟不上 | 方法调用层次太深,栈帧太多 |
| 典型场景 | 无限循环创建对象、大文件加载、内存泄漏 | 无限递归、循环调用(A→B→A) |
| 错误信息 | Java heap space / Metaspace | stack trace 很长,重复出现相同方法 |
| 能否恢复 | 通常不可恢复(需重启) | 理论上可捕获,但通常也需修复代码 |
二、OutOfMemoryError(堆溢出)------ "仓库爆仓"
原理
堆是存放对象实例的地方。如果不断创建对象,且这些对象一直被引用(无法被 GC 回收),最终堆内存耗尽。
代码示例 1:无限创建对象
java
public class HeapOOM {
static class BigObject {
byte[] data = new byte[1024 * 1024]; // 1MB 的大对象
}
public static void main(String[] args) {
List<BigObject> list = new ArrayList<>();
// 无限循环添加大对象
while (true) {
list.add(new BigObject()); // 一直占用内存,无法回收
}
}
}
报错信息:
Exception in thread "main" java.lang.OutOfMemoryError: Java heap space
代码示例 2:内存泄漏(无意识持有引用)
java
public class MemoryLeak {
static List<byte[]> leakList = new ArrayList<>();
public void process() {
// 每次调用都往静态列表加数据,但从不清理
byte[] data = new byte[1024 * 1024]; // 1MB
leakList.add(data); // 静态引用导致无法 GC
}
public static void main(String[] args) {
MemoryLeak ml = new MemoryLeak();
for (int i = 0; i < 10000; i++) {
ml.process(); // 很快耗尽内存
}
}
}
三、StackOverflowError(栈溢出)------ "俄罗斯套娃"
原理
每个线程有一个栈 ,用于存储方法调用的栈帧 (局部变量、返回地址等)。方法调用一次就压入一层,返回时弹出。如果无限递归 或循环调用,栈深度超过限制(默认约 1MB),就会撑爆。
代码示例 1:无限递归
java
public class StackOverflow {
// 递归计算阶乘,但忘记写终止条件!
public int factorial(int n) {
return n * factorial(n - 1); // 永远递归下去,没有出口
}
public static void main(String[] args) {
new StackOverflow().factorial(5);
}
}
报错信息(关键特征:方法名重复出现):
java
Exception in thread "main" java.lang.StackOverflowError
at StackOverflow.factorial(StackOverflow.java:5)
at StackOverflow.factorial(StackOverflow.java:5)
at StackOverflow.factorial(StackOverflow.java:5)
... (重复几百次)
代码示例 2:循环依赖调用
java
public class CircularCall {
public void methodA() {
methodB(); // A 调用 B
}
public void methodB() {
methodA(); // B 又调用 A,死循环
}
public static void main(String[] args) {
new CircularCall().methodA();
}
}
四、一句话区分
OOM :
new了太多对象,堆(Heap)说"我满了,装不下了"。栈溢出:方法调用太深,栈(Stack)说"我叠太高了,要塌了"。
记忆口诀:
-
OOM = O bjects O verwhelming Memory(对象淹没内存)
-
StackOverflow = S illy T oo A bundant C all Keys(愚蠢的过多调用键)
五、解决方案
|---------|---------------------------------------------------------------------------------------|
| 问题 | 解决思路 |
| OOM | 1. 增加堆内存(-Xmx2g) 2. 检查内存泄漏(使用 MAT 工具分析 dump 文件) 3. 优化代码(及时释放引用、使用软引用) 4. 分页/流式处理大数据 |
| 栈溢出 | 1. 检查递归终止条件 2. 将递归改为循环(尾递归优化) 3. 增加栈深度(-Xss2m,治标不治本) 4. 检查循环依赖 |
核心区别:一个逼你"必须处理",一个"可以不管但可能运行时炸"。
一、本质对比
|-----------|-----------------------------|---------------------------------------|
| 维度 | 运行时异常(RuntimeException) | 编译时异常(Checked Exception) |
| 别称 | 非受检异常(Unchecked) | 受检异常(Checked) |
| 继承关系 | 继承 RuntimeException | 继承 Exception(但不继承 RuntimeException) |
| 编译器态度 | 不强制处理,编译通过 | 强制处理,否则编译报错 |
| 出现时机 | 运行时才暴露(代码逻辑错误) | 编译期就必须考虑(外部环境问题) |
| 典型例子 | 空指针、数组越界、类型转换错误 | 文件找不到、数据库连不上、网络超时 |
| 责任归属 | 程序员代码写错了(可避免) | 外部环境不可控(必须预案) |
二、运行时异常(RuntimeException)
特征 :代码逻辑错误,理论上通过严谨编码可以避免,所以编译器不强迫你处理。
常见类型(图片中的例子)
java
// 1. NullPointerException(空指针)- 最常见
String s = null;
s.length(); // 运行时才发现 s 是 null
// 2. ArrayIndexOutOfBoundsException(数组越界)
int[] arr = new int[5];
arr[10] = 1; // 运行时才发现索引 10 不存在
// 3. ClassCastException(类型强转错误)
Object obj = "hello";
Integer num = (Integer) obj; // 运行时才发现 String 不能转 Integer
// 4. ArithmeticException(算术错误)
int x = 10 / 0; // 运行时才发现除数为 0
处理方式:
-
可以不 try-catch(编译器不管),但运行时会崩溃
-
推荐做法 :用
if判断避免,而非捕获异常
java
// 好的做法:提前判断(防御式编程)
if (s != null) {
s.length();
}
// 坏的做法:依赖 try-catch 控制流程
try {
s.length();
} catch (NullPointerException e) {
// 用异常处理业务逻辑,性能差且代码丑
}
三、编译时异常(Checked Exception)
特征 :外部环境风险,即使代码逻辑正确也可能发生(如硬盘坏了、断网了),编译器强迫你必须处理。
常见类型(图片中的例子)
java
// 1. IOException(IO 错误)
FileReader file = new FileReader("不存在的文件.txt");
// 编译报错:Unhandled exception: java.io.FileNotFoundException
// 2. SQLException(数据库错误)
Connection conn = DriverManager.getConnection("jdbc:mysql://...", "user", "pwd");
// 编译报错:Unhandled exception: java.sql.SQLException
处理方式(二选一):
方式 1:try-catch 捕获
java
try {
FileReader file = new FileReader("data.txt");
// 读取文件...
} catch (FileNotFoundException e) {
System.out.println("文件没找到,请检查路径");
// 或者记录日志、给用户友好提示
}
方式 2:throws 抛出(交给上层处理)
java
// 方法声明:我不处理,谁调用我谁处理
public void readData() throws FileNotFoundException {
FileReader file = new FileReader("data.txt");
// ...
}
// 调用方必须处理
public static void main(String[] args) {
try {
new Demo().readData();
} catch (FileNotFoundException e) {
e.printStackTrace();
}
}
四、设计哲学:为什么这样区分?
运行时异常(RuntimeException):
"这是你的错,你应该改代码避免,我不逼你处理,但炸了别怪我"
-
空指针、数组越界 → 写代码时检查好就行
-
如果强制 try-catch,代码会充斥大量无意义的捕获,导致臃肿
编译时异常(Checked Exception):
"这是外部环境的风险,你必须给个预案,否则编译都不让你过"
-
文件可能不存在、网络可能断开 → 不是代码错,是现实不确定
-
强制处理确保程序员考虑容错(如提示用户"文件找不到"而非直接崩溃)
五、一句话总结
|-----------|---------------|------------------------------|
| 类型 | 记忆口诀 | 处理策略 |
| 运行时异常 | "代码烂,自己修" | 用 if 提前防,尽量别靠 try-catch |
| 编译时异常 | "环境差,必须备" | 必须 try-catch 或 throws,做好降级方案 |
典型错误:
-
把运行时异常用 try-catch 包起来(应该改代码判空)
-
把编译时异常忽略不处理(编译器会报错,无法运行)