Skip to content

异常体系结构

凌晨 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("清理资源");
}

执行顺序

正常情况:tryfinally

有异常:trycatchfinally

记住: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);
}

这样上层能拿到完整的堆栈信息,排查问题时不会两眼一抹黑。

基于 VitePress 构建