异常体系结构
凌晨 2 点,你盯着屏幕上的一行报错:NullPointerException at line 42。这时候你最想知道的是:这个异常从哪来的,为什么会发生。
要回答这两个问题,得先搞清楚 Java 的异常是怎么组织的。
Throwable 层次结构
java.lang.Throwable
│
├── java.lang.Error(错误)
│ ├── VirtualMachineError
│ │ └── OutOfMemoryError
│ ├── StackOverflowError
│ └── AssertionError
│
└── java.lang.Exception(异常)
│
├── RuntimeException(运行时异常)
│ ├── NullPointerException
│ ├── IndexOutOfBoundsException
│ ├── ClassCastException
│ ├── ArithmeticException
│ └── IllegalArgumentException
│
└── IOException(受检异常)
├── FileNotFoundException
├── EOFException
└── SocketException三种异常类型
受检异常(Checked Exception)
编译器强制要求处理的异常。如果不处理,代码根本编译不过。
java
// IOException 是受检异常,必须处理
try {
FileReader reader = new FileReader("test.txt");
reader.read();
reader.close();
} catch (FileNotFoundException e) {
System.out.println("文件不存在");
} catch (IOException e) {
System.out.println("读取失败");
}说白了,这是 Java 设计者认为「调用者应该关心的异常」。文件找不到、网络断了——这些确实可能发生,处理一下是合理的。
运行时异常(Runtime Exception)
程序运行时才暴露的异常,通常是编程错误:
java
int[] arr = {1, 2, 3};
arr[10] = 100; // ArrayIndexOutOfBoundsException - 数组越界
String str = null;
str.length(); // NullPointerException - 空指针
int num = 10 / 0; // ArithmeticException - 算术错误这类异常不强制处理,因为它们往往是自己的锅:写代码时检查一下就能避免。
错误(Error)
JVM 本身的错误,通常无法恢复:
java
// OutOfMemoryError - 内存不足
// StackOverflowError - 栈溢出
// NoClassDefFoundError - 类找不到
// 一般不应该捕获 Error,捕获了也干不了什么
try {
// ...
} catch (Error e) { // 别这么干
e.printStackTrace();
}遇到 Error 最好的做法是让它终止程序,然后去修复底层的代码问题或配置问题。
try-catch-finally
java
try {
// 可能抛出异常的代码
FileReader reader = new FileReader("test.txt");
reader.read();
} catch (FileNotFoundException e) {
// 文件不存在
System.out.println("文件不存在:" + e.getMessage());
} catch (IOException e) {
// IO 异常
System.out.println("读取失败:" + e.getMessage());
} finally {
// 无论是否异常都会执行
System.out.println("清理资源");
}执行顺序
正常情况:try → finally
有异常:try → catch → finally
记住:finally 永远会执行,除非 System.exit() 终止了 JVM。
多异常捕获(JDK 7+)
java
try {
// 可能抛出多种异常
} catch (IOException | SQLException e) { // JDK 7+
// 处理逻辑一样,就合并写
e.printStackTrace();
}多个异常类型用 | 分隔,处理逻辑相同时可以少写代码。
try-with-resources(JDK 7+)
写文件操作时最常见的坑:忘记关闭流、关闭时抛异常、死活找不到问题在哪。try-with-resources 就是来解决这个的。
java
// 传统方式:容易漏掉 close
FileReader reader = null;
try {
reader = new FileReader("test.txt");
reader.read();
} finally {
if (reader != null) {
try {
reader.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
// try-with-resources:自动关闭
try (FileReader reader = new FileReader("test.txt")) {
reader.read();
} catch (IOException e) {
e.printStackTrace();
}多个资源用分号分隔:
java
try (
FileReader reader = new FileReader("input.txt");
FileWriter writer = new FileWriter("output.txt")
) {
writer.write(reader.read());
} catch (IOException e) {
e.printStackTrace();
}资源按声明的相反顺序关闭。
throw 与 throws
throw - 抛出异常
在方法内部使用,主动抛出一个异常:
java
public void validate(int age) {
if (age < 0) {
throw new IllegalArgumentException("年龄不能为负数");
}
if (age > 150) {
throw new IllegalArgumentException("年龄超出范围");
}
}throws - 声明异常
在方法签名上使用,告诉调用者这个方法可能抛出什么异常:
java
// 告诉调用者:我可能抛出 IOException
public void readFile(String path) throws IOException {
FileReader reader = new FileReader(path);
reader.read();
}
// 多个异常用逗号分隔
public void method() throws IOException, SQLException {
// ...
}自定义异常
定义异常类
java
// 运行时异常(最常用)
public class BusinessException extends RuntimeException {
public BusinessException(String message) {
super(message);
}
public BusinessException(String message, Throwable cause) {
super(message, cause);
}
}
// 受检异常(需要强制处理)
public class DataAccessException extends Exception {
public DataAccessException(String message) {
super(message);
}
public DataAccessException(String message, Throwable cause) {
super(message, cause);
}
}命名规范:以 Exception 结尾。
使用场景
java
public class UserService {
public User findById(Long id) {
if (id == null) {
throw new IllegalArgumentException("ID 不能为空");
}
User user = userRepository.findById(id);
if (user == null) {
throw new UserNotFoundException("用户不存在:" + id);
}
return user;
}
}最佳实践
捕获具体异常
java
// ❌ 别这样:捕获所有异常,丢失具体信息
try {
// ...
} catch (Exception e) {
e.printStackTrace();
}
// ✅ 这样做:捕获具体异常
try {
// ...
} catch (FileNotFoundException e) {
// 文件不存在
} catch (IOException e) {
// IO 错误
}不要吞掉异常
java
// ❌ 吞掉异常:出了 bug 都不知道在哪
try {
// ...
} catch (Exception e) {
// 什么都不做
}
// ✅ 至少记录日志
try {
// ...
} catch (Exception e) {
log.error("操作失败", e);
throw e; // 或者重新抛出
}优先使用标准异常
java
// ❌ 用 Exception 太笼统
if (name == null) {
throw new Exception("名称不能为空");
}
// ✅ 用标准异常更清晰
if (name == null) {
throw new IllegalArgumentException("名称不能为空");
}
if (list == null || list.isEmpty()) {
throw new NoSuchElementException("列表为空");
}不要用异常控制流程
java
// ❌ 用异常做流程控制——慢、丑、容易出错
try {
while (true) {
list.remove(0);
}
} catch (IndexOutOfBoundsException e) {
// 列表为空时退出
}
// ✅ 正常判断:清晰、快速
while (!list.isEmpty()) {
list.remove(0);
}保留原始异常(异常链)
java
try {
dao.save(user);
} catch (SQLException e) {
// 抛出业务异常,保留原始异常信息
throw new BusinessException("保存用户失败", e);
}这样上层能拿到完整的堆栈信息,排查问题时不会两眼一抹黑。
