📚 本系列系统梳理了 Java 开发的详细知识点,从基础语法到工程实践层层递进,内容详实成体系,建议先收藏再慢慢阅读,方便日后随时回顾查阅。

前言

Java 的异常体系是它最具争议的设计之一——Checked Exception 到底是好是坏,至今没有定论。但不管你怎么看,实际开发中必须和它打交道。这篇文章把异常的分类、处理机制、最佳实践一次讲清。

1. 异常的类层次

Java 中所有异常都是对象,继承自 Throwable

Throwable
├── Error(系统级错误,不应该捕获)
│   ├── OutOfMemoryError
│   ├── StackOverflowError
│   └── ...
└── Exception(程序级异常)
    ├── RuntimeException(Unchecked,运行时异常)
    │   ├── NullPointerException
    │   ├── ArrayIndexOutOfBoundsException
    │   ├── ClassCastException
    │   ├── IllegalArgumentException
    │   ├── ArithmeticException
    │   ├── NumberFormatException
    │   ├── UnsupportedOperationException
    │   └── ...
    └── 其他 Exception(Checked,受检异常)
        ├── IOException
        ├── SQLException
        ├── FileNotFoundException
        ├── ClassNotFoundException
        └── ...

1.1 三大分类

分类 父类 是否必须处理 典型场景
Error Error 不需要(也处理不了) JVM 内存耗尽、栈溢出
Checked Exception Exception(非 RuntimeException) 编译器强制要求 try-catch 或 throws 文件不存在、网络中断、SQL 错误
Unchecked Exception RuntimeException 不强制,可以不处理 空指针、数组越界、类型转换失败

一句话区分:Checked 是"可以预见并应该处理的外部问题",Unchecked 是"程序员写了 bug"。

2. 异常处理语法

2.1 try-catch-finally

try {
    String s = null;
    s.length();                    // 抛出 NullPointerException
} catch (NullPointerException e) {
    System.out.println("空指针:" + e.getMessage());
} catch (Exception e) {
    System.out.println("其他异常:" + e.getMessage());
} finally {
    System.out.println("无论是否异常都会执行");
    // 常用于关闭资源(但现代 Java 更推荐 try-with-resources)
}

2.2 多异常捕获(Java 7+)

try {
    // 可能抛出多种异常
} catch (IOException | SQLException e) {
    // 用 | 合并处理,e 的类型是这些异常的共同父类
    System.out.println("IO 或 SQL 异常:" + e.getMessage());
}

2.3 throws 声明

如果你不想在当前方法处理异常,可以用 throws 抛给调用者:

// Checked 异常:必须声明或捕获,否则编译报错
public String readFile(String path) throws IOException {
    return new String(Files.readAllBytes(Paths.get(path)));
}

// 调用者必须处理
try {
    String content = readFile("data.txt");
} catch (IOException e) {
    e.printStackTrace();
}

// Unchecked 异常:不需要声明(但可以声明,作为文档提示)
public int divide(int a, int b) {
    return a / b;  // 可能抛 ArithmeticException,但不用声明
}

2.4 throw 手动抛出异常

public void setAge(int age) {
    if (age < 0 || age > 150) {
        throw new IllegalArgumentException("Invalid age: " + age);
    }
    this.age = age;
}

// throw vs throws:
// throw  → 在方法体内抛出一个具体的异常对象
// throws → 在方法签名上声明可能抛出的异常类型

3. try-with-resources(Java 7+)

3.1 传统写法的痛苦

在 Java 7 之前,关闭资源需要在 finally 中手写,非常啰嗦且容易遗漏:

// 传统写法:丑陋且容易出错
BufferedReader reader = null;
try {
    reader = new BufferedReader(new FileReader("data.txt"));
    String line = reader.readLine();
} catch (IOException e) {
    e.printStackTrace();
} finally {
    if (reader != null) {
        try {
            reader.close();  // close 本身也可能抛异常
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

3.2 try-with-resources 自动关闭

只要资源实现了 AutoCloseable 接口,就可以放在 try() 括号中,作用域结束后自动调用 close()

// 现代写法:简洁、安全
try (BufferedReader reader = new BufferedReader(new FileReader("data.txt"))) {
    String line = reader.readLine();
    System.out.println(line);
} catch (IOException e) {
    e.printStackTrace();
}
// reader 在这里已经自动关闭,不需要 finally

// 多个资源用分号隔开
try (
    FileInputStream fis = new FileInputStream("input.txt");
    FileOutputStream fos = new FileOutputStream("output.txt")
) {
    fos.write(fis.readAllBytes());
}
// fis 和 fos 都会自动关闭,关闭顺序和声明顺序相反(后声明的先关)

3.3 哪些类可以用 try-with-resources?

所有实现了 AutoCloseable(或其子接口 Closeable)的类,包括:

// 常见的可自动关闭资源
InputStream / OutputStream       // 文件、网络流
Reader / Writer                  // 字符流
Connection / Statement / ResultSet  // JDBC 数据库
Socket / ServerSocket            // 网络
Scanner                          // 输入扫描

// 自定义可关闭资源
public class MyResource implements AutoCloseable {
    public void doWork() {
        System.out.println("working...");
    }

    @Override
    public void close() {
        System.out.println("resource closed.");
    }
}

try (MyResource r = new MyResource()) {
    r.doWork();
}
// 输出:
// working...
// resource closed.

4. 自定义异常

4.1 什么时候需要自定义?

当标准异常无法准确表达你的业务语义时。比如"用户余额不足"用 IllegalStateException 也行,但自定义的 InsufficientBalanceException 调用者一看就懂,还能携带业务信息。

4.2 怎么写?

// Unchecked 自定义异常:继承 RuntimeException
public class InsufficientBalanceException extends RuntimeException {
    private final double balance;
    private final double amount;

    public InsufficientBalanceException(double balance, double amount) {
        super(String.format("余额不足:当前 %.2f,需要 %.2f", balance, amount));
        this.balance = balance;
        this.amount = amount;
    }

    public double getBalance() { return balance; }
    public double getAmount() { return amount; }
}

// 使用
public void withdraw(double amount) {
    if (balance < amount) {
        throw new InsufficientBalanceException(balance, amount);
    }
    balance -= amount;
}
// Checked 自定义异常:继承 Exception
public class FileParseException extends Exception {
    private final int lineNumber;

    public FileParseException(String message, int lineNumber) {
        super(message);
        this.lineNumber = lineNumber;
    }

    public FileParseException(String message, int lineNumber, Throwable cause) {
        super(message, cause);  // 保留原始异常链
        this.lineNumber = lineNumber;
    }

    public int getLineNumber() { return lineNumber; }
}

4.3 选 Checked 还是 Unchecked?

场景 选择 原因
调用者可以合理地恢复处理 Checked 强制调用者面对这个问题
程序错误(参数非法、状态异常) Unchecked 不应该让调用者 try-catch 来掩盖 bug
不确定 Unchecked 现代 Java 开发和 Spring 生态的主流倾向

5. 异常链(Chained Exceptions)

捕获底层异常后抛出高层异常时,务必保留原始原因:

// 错误:丢失了原始异常信息
try {
    readFile("data.txt");
} catch (IOException e) {
    throw new RuntimeException("处理失败");  // 原始的 IOException 丢了
}

// 正确:通过 cause 保留异常链
try {
    readFile("data.txt");
} catch (IOException e) {
    throw new RuntimeException("处理失败", e);  // e 作为 cause 传入
}

// 排查问题时可以追溯完整链路:
// RuntimeException: 处理失败
//   Caused by: java.io.IOException: No such file
//     Caused by: java.io.FileNotFoundException: data.txt

6. 异常处理的最佳实践

6.1 不要吞掉异常

// 最差的写法:异常被吞掉了,出了问题完全无法排查
try {
    riskyOperation();
} catch (Exception e) {
    // 什么都不做
}

// 至少要记录日志
try {
    riskyOperation();
} catch (Exception e) {
    log.error("操作失败", e);  // 或 e.printStackTrace() 用于调试
}

6.2 不要用异常控制流程

// 错误:用异常代替条件判断,性能差且意图不清
try {
    int value = Integer.parseInt(input);
} catch (NumberFormatException e) {
    value = 0;  // 把异常当 if-else 用
}

// 正确:先检查
if (input != null && input.matches("-?\\d+")) {
    value = Integer.parseInt(input);
} else {
    value = 0;
}

6.3 精确捕获,不要一把梭

// 错误:捕获范围太大,可能掩盖其他 bug
try {
    String s = map.get(key).trim().toLowerCase();
} catch (Exception e) {
    return "";  // NullPointerException? ClassCastException? 全吞了
}

// 正确:只捕获你预期的异常
try {
    String s = map.get(key).trim().toLowerCase();
} catch (NullPointerException e) {
    return "";  // 明确处理 key 不存在的情况
}

// 更好:根本不用异常
String raw = map.get(key);
return raw != null ? raw.trim().toLowerCase() : "";

6.4 finally 中不要 return

// 危险:finally 中的 return 会覆盖 try/catch 中的返回值和异常
public int dangerous() {
    try {
        throw new RuntimeException("出错了!");
    } finally {
        return 0;  // 异常被吞掉了,方法正常返回 0,调用者完全不知道出了问题
    }
}

6.5 常见的异常使用场景速查

异常 使用场景
IllegalArgumentException 方法参数不合法
IllegalStateException 对象状态不对(如未初始化就调用)
NullPointerException 通常是 bug,避免手动 throw
UnsupportedOperationException 不支持的操作(如不可变集合的 add)
IndexOutOfBoundsException 索引越界
ConcurrentModificationException 遍历时修改集合
IOException I/O 操作失败
NumberFormatException 字符串转数字格式错误

7. 小结

主题 关键要点
三大分类 Error(别管)、Checked(编译器强制处理)、Unchecked(运行时异常)
try-catch 多异常可用 | 合并;catch 顺序从子类到父类
try-with-resources 实现 AutoCloseable 的资源自动关闭,替代 finally 关资源
throw / throws throw 抛具体异常,throws 在签名上声明
自定义异常 不确定时选 Unchecked;携带业务信息;保留异常链
最佳实践 不吞异常、不用异常控制流程、精确捕获、finally 中不 return

下一篇预告:Lambda 与 Stream API——Java 8 函数式编程的核心工具


🎯 如果这篇文章对你有帮助,别忘了点赞、收藏、关注三连!关注我,让你在 Java 学习的道路上不迷路,持续为你带来成体系的 Java 干货~

更多推荐