知识模块
☕ Java 知识模块
一、Java 基础
异常处理

异常处理

异常处理是 Java 程序健壮性的重要保障。理解异常体系、掌握异常处理最佳实践,是 Java 开发者的必备技能。


一、异常体系

1.1 异常继承结构

Java 异常体系的核心是 Throwable 类,所有异常都继承自它。

                    ┌─────────────┐
                    │  Throwable  │
                    └──────┬──────┘

            ┌──────────────┴──────────────┐
            │                             │
     ┌──────┴──────┐               ┌──────┴──────┐
     │    Error    │               │  Exception  │
     └──────┬──────┘               └──────┬──────┘
            │                             │
    ┌───────┼───────┐         ┌───────────┼───────────┐
    │       │       │         │           │           │
┌───┴───┐ ┌─┴───┐ ┌─┴───┐ ┌───┴───┐ ┌─────┴─────┐ ┌───┴───┐
│Virtual │ │OutOf│ │Stack │ │Runtime│ │  IOException│ │  SQL  │
│Machine │ │Memory│ │Over- │ │Exception│ │            │ │Exception│
│Error   │ │Error │ │flow  │ │        │ │            │ │        │
└────────┘ └─────┘ └──────┘ └────┬───┘ └────────────┘ └───────┘

                          ┌───────┼───────┐
                          │       │       │
                      ┌───┴───┐ ┌─┴───┐ ┌─┴───────┐
                      │NullP- │ │Array│ │ClassCast│
                      │ointer │ │Index│ │Exception│
                      │Except-│ │Out- │ │         │
                      │ion    │ │Bound│ │         │
                      └───────┘ └─────┘ └─────────┘

1.2 三大类别的区别

类别说明特点处理方式
Error系统级错误JVM 无法恢复的严重问题无法处理,程序终止
Checked Exception受检异常编译器强制处理必须捕获或声明抛出
Unchecked Exception非受检异常编译器不强制处理可选捕获或抛出
// Error 示例:通常不需要捕获
OutOfMemoryError        // 内存溢出
StackOverflowError      // 栈溢出(递归过深)
VirtualMachineError     // 虚拟机错误
 
// Checked Exception 示例:必须处理
IOException             // IO 异常
SQLException            // 数据库异常
ClassNotFoundException  // 类找不到
FileNotFoundException   // 文件不存在
 
// Unchecked Exception(RuntimeException 子类):可选处理
NullPointerException        // 空指针
ArrayIndexOutOfBoundsException // 数组越界
ClassCastException           // 类型转换异常
ArithmeticException          // 算术异常(如除零)
IllegalArgumentException     // 非法参数
NumberFormatException        // 数字格式异常

1.3 Error vs Exception

对比项ErrorException
含义系统级错误程序级错误
来源JVM应用程序
可恢复性不可恢复可恢复
处理方式不建议捕获必须处理或声明
典型例子OutOfMemoryErrorNullPointerException

1.4 Checked vs Unchecked Exception

对比项Checked ExceptionUnchecked Exception
父类Exception(非 RuntimeException)RuntimeException
编译检查强制处理不强制
设计理念可预期的异常编程错误
例子IOException, SQLExceptionNPE, ArrayIndexOutOfBounds
// Checked Exception:编译器强制处理
public void readFile(String path) throws IOException {
    FileReader reader = new FileReader(path);  // 必须处理或声明抛出
}
 
// Unchecked Exception:编译器不强制
public void process(String s) {
    System.out.println(s.length());  // 可能 NPE,但编译器不强制处理
}

二、try-catch-finally

2.1 基本语法

try {
    // 可能抛出异常的代码
} catch (ExceptionType1 e) {
    // 处理 ExceptionType1 类型的异常
} catch (ExceptionType2 e) {
    // 处理 ExceptionType2 类型的异常
} finally {
    // 无论是否异常都会执行(可选)
}

2.2 执行流程

┌─────────────────────────────────────────────┐
│                  try 块                     │
│                                             │
│   ┌─────────────┐      ┌─────────────┐     │
│   │ 正常执行完毕 │  或  │ 抛出异常    │     │
│   └──────┬──────┘      └──────┬──────┘     │
└──────────┼────────────────────┼────────────┘
           │                    │
           │              匹配 catch
           │                    │
           │              ┌─────┴─────┐
           │              │  catch 块 │
           │              └─────┬─────┘
           │                    │
           └────────┬───────────┘

              ┌─────┴─────┐
              │ finally 块│  ← 无论是否异常都执行
              └───────────┘

2.3 多重捕获

// 多个 catch 块(从具体到宽泛)
try {
    // ...
} catch (FileNotFoundException e) {
    System.out.println("文件不存在");
} catch (IOException e) {
    System.out.println("IO 异常");
} catch (Exception e) {
    System.out.println("其他异常");
}
 
// JDK 7+ 多异常捕获
try {
    // ...
} catch (FileNotFoundException | SQLException e) {
    System.out.println("文件或数据库异常");
}

2.4 finally 块

finally 的执行特点

  1. 无论是否发生异常,finally 都会执行
  2. 即使 try 或 catch 中有 return,finally 也会执行
  3. finally 中的 return 会覆盖 try/catch 中的 return
// 示例 1:finally 总是执行
public void test() {
    try {
        System.out.println("try");
        return;
    } finally {
        System.out.println("finally");  // 仍然执行
    }
}
// 输出:try → finally
 
// 示例 2:finally 覆盖返回值
public int test() {
    try {
        return 1;
    } finally {
        return 2;  // 覆盖 try 中的 return
    }
}
// 返回 2
 
// 示例 3:finally 不执行的情况
try {
    System.exit(0);  // JVM 退出,finally 不执行
} finally {
    System.out.println("不会执行");
}

2.5 try-with-resources(JDK 7+)

实现了 AutoCloseable 接口的资源可以自动关闭。

// 传统方式
FileInputStream fis = null;
try {
    fis = new FileInputStream("test.txt");
    // 使用资源
} finally {
    if (fis != null) {
        fis.close();  // 手动关闭
    }
}
 
// try-with-resources(推荐)
try (FileInputStream fis = new FileInputStream("test.txt")) {
    // 使用资源
} // 自动关闭
 
// 多个资源
try (
    FileInputStream fis = new FileInputStream("in.txt");
    FileOutputStream fos = new FileOutputStream("out.txt")
) {
    // 使用资源
} // 自动关闭(按声明逆序关闭)

2.6 常见陷阱

陷阱 1:finally 覆盖异常

public void test() {
    try {
        throw new RuntimeException("try 异常");
    } finally {
        throw new RuntimeException("finally 异常");  // 覆盖 try 中的异常
    }
}
// 抛出 "finally 异常","try 异常" 被吞掉

陷阱 2:finally 覆盖返回值

public int test() {
    int x = 1;
    try {
        return x;
    } finally {
        x = 2;  // 修改 x 不影响返回值(返回值已缓存)
    }
}
// 返回 1
 
public int test() {
    try {
        return 1;
    } finally {
        return 2;  // 直接 return 会覆盖
    }
}
// 返回 2

三、throw 与 throws

3.1 throw 关键字

throw 用于主动抛出异常

public void setAge(int age) {
    if (age < 0 || age > 150) {
        throw new IllegalArgumentException("年龄必须在 0-150 之间");
    }
    this.age = age;
}
 
// 抛出自定义异常
public void withdraw(double amount) throws InsufficientBalanceException {
    if (amount > balance) {
        throw new InsufficientBalanceException("余额不足");
    }
    balance -= amount;
}

3.2 throws 关键字

throws 用于方法签名中声明可能抛出的异常

// 声明抛出 Checked Exception
public void readFile(String path) throws IOException, FileNotFoundException {
    FileReader reader = new FileReader(path);
}
 
// 声明抛出多个异常
public void connect() throws IOException, SQLException {
    // ...
}
 
// 子类重写方法时,抛出异常的限制
class Parent {
    public void method() throws IOException { }
}
 
class Child extends Parent {
    // 重写方法:可以抛出相同异常或子类异常,或不抛出
    @Override
    public void method() throws FileNotFoundException { }  // ✅ IOException 子类
    
    // @Override
    // public void method() throws Exception { }  // ❌ 编译错误:不能抛出更宽泛的异常
}

3.3 throw vs throws

对比项throwthrows
位置方法体内方法签名后
作用抛出异常对象声明可能抛出的异常类型
后面跟什么异常对象(一个)异常类型(可多个)
使用场景主动抛出异常声明受检异常
// throw:主动抛出
public void validate(String name) {
    if (name == null) {
        throw new NullPointerException("名称不能为空");  // 抛出对象
    }
}
 
// throws:声明异常
public void process() throws IOException, SQLException {  // 声明类型
    // 可能抛出 IOException 或 SQLException
}

四、自定义异常

4.1 创建自定义异常

// 自定义受检异常
public class InsufficientBalanceException extends Exception {
    
    public InsufficientBalanceException() {
        super();
    }
    
    public InsufficientBalanceException(String message) {
        super(message);
    }
    
    public InsufficientBalanceException(String message, Throwable cause) {
        super(message, cause);
    }
}
 
// 自定义非受检异常
public class InvalidParameterException extends RuntimeException {
    
    public InvalidParameterException(String message) {
        super(message);
    }
}

4.2 使用自定义异常

public class BankAccount {
    private double balance;
    
    public void withdraw(double amount) throws InsufficientBalanceException {
        if (amount > balance) {
            throw new InsufficientBalanceException(
                "余额不足:当前余额 " + balance + ",取款金额 " + amount
            );
        }
        balance -= amount;
    }
}
 
// 使用
BankAccount account = new BankAccount();
try {
    account.withdraw(1000);
} catch (InsufficientBalanceException e) {
    System.out.println(e.getMessage());
}

4.3 设计原则

原则说明
选择正确的父类可恢复 → Checked Exception;编程错误 → Unchecked Exception
提供有意义的构造器无参、带消息、带原因
保留异常链使用 cause 参数保留原始异常
命名规范Exception 结尾
// 保留异常链
public class DataAccessException extends Exception {
    public DataAccessException(String message, Throwable cause) {
        super(message, cause);  // 保留原始异常
    }
}
 
// 使用
try {
    // 数据库操作
} catch (SQLException e) {
    throw new DataAccessException("数据访问失败", e);  // 保留原始异常
}

五、异常处理最佳实践

5.1 应该做的

实践说明
只捕获能处理的异常不要为了通过编译而捕获
保留原始异常信息使用异常链
尽早抛出异常fail-fast 原则
使用具体异常类型避免直接 catch Exception
记录异常日志便于问题排查
合理使用自定义异常提供业务语义
// ✅ 正确做法
 
// 1. 保留原始异常
try {
    // ...
} catch (SQLException e) {
    throw new BusinessException("操作失败", e);  // 保留原因
}
 
// 2. 尽早抛出
public void setName(String name) {
    if (name == null) {
        throw new IllegalArgumentException("名称不能为空");  // 尽早失败
    }
    this.name = name;
}
 
// 3. 使用具体异常
try {
    // ...
} catch (FileNotFoundException e) {
    // 处理文件不存在
} catch (IOException e) {
    // 处理其他 IO 异常
}
 
// 4. 记录日志
try {
    // ...
} catch (Exception e) {
    log.error("操作失败", e);
    throw e;
}

5.2 不应该做的

反模式说明
捕获后不处理吞掉异常,隐藏问题
捕获 Exception太宽泛,可能捕获不应捕获的异常
finally 中 return覆盖返回值和异常
异常代替流程控制性能差,代码可读性低
忽略 Checked Exception空 catch 块
// ❌ 错误做法
 
// 1. 吞掉异常
try {
    // ...
} catch (Exception e) {
    // 空块,异常被忽略
}
 
// 2. 捕获太宽泛
try {
    // ...
} catch (Exception e) {  // 捕获了所有异常,包括 Error
    e.printStackTrace();
}
 
// 3. finally 中 return
public int test() {
    try {
        return 1;
    } finally {
        return 2;  // 覆盖返回值
    }
}
 
// 4. 异常代替流程控制
public boolean isNumber(String s) {
    try {
        Integer.parseInt(s);
        return true;
    } catch (NumberFormatException e) {
        return false;  // 不推荐
    }
}
 
// 5. 空 catch
try {
    // ...
} catch (IOException e) {
    // 什么都不做
}

5.3 异常处理原则

┌────────────────────────────────────────────┐
│           异常处理决策树                    │
├────────────────────────────────────────────┤
│                                            │
│   能处理这个异常吗?                        │
│        │                                   │
│   ┌────┴────┐                              │
│   能        不能                           │
│   │         │                              │
│   ↓         ↓                              │
│ 处理它    声明抛出(throws)                │
│   │         │                              │
│   ↓         需要转换异常吗?                │
│ 继续执行       │                            │
│            ┌──┴──┐                         │
│           是     否                        │
│            │      │                        │
│            ↓      ↓                        │
│       包装抛出   直接抛出                   │
│                                            │
└────────────────────────────────────────────┘

六、异常链

6.1 什么是异常链

异常链(Exception Chaining)是指在捕获一个异常后,抛出另一个异常,同时保留原始异常信息。

public class BusinessException extends Exception {
    public BusinessException(String message, Throwable cause) {
        super(message, cause);  // 保留原始异常
    }
}
 
// 使用
try {
    // 数据库操作
} catch (SQLException e) {
    throw new BusinessException("业务操作失败", e);  // 保留 SQLException 作为原因
}
 
// 获取原始异常
catch (BusinessException e) {
    System.out.println(e.getMessage());        // "业务操作失败"
    System.out.println(e.getCause());          // SQLException
    e.printStackTrace();                       // 打印完整异常链
}

6.2 异常链的作用

作用说明
保留根因不丢失原始异常信息
语义转换将底层异常转换为业务异常
便于排查完整的异常调用链

七、常见面试题

Q1: Error 和 Exception 有什么区别?

A:

  • Error:系统级错误,JVM 无法恢复,程序通常终止(如 OutOfMemoryError)
  • Exception:程序级错误,可以捕获处理,程序可以继续运行

Q2: Checked Exception 和 Unchecked Exception 的区别?

A:

对比项CheckedUnchecked
父类Exception(非 RuntimeException)RuntimeException
编译检查强制处理不强制
代表IOException, SQLExceptionNPE, ArrayIndexOutOfBounds

Q3: finally 块一定会执行吗?

A: 不一定。以下情况 finally 不会执行:

  • System.exit() 调用
  • JVM 崩溃
  • 线程被杀死(如 Thread.stop()

Q4: try 块中有 return,finally 还会执行吗?

A: 会。finally 在 return 之前执行,但如果 finally 中也有 return,会覆盖 try 中的返回值。

public int test() {
    try {
        return 1;
    } finally {
        return 2;  // 最终返回 2
    }
}

Q5: 以下代码输出什么?

public static int test() {
    int x = 1;
    try {
        return x++;
    } finally {
        x++;
    }
}
System.out.println(test());

A: 输出 1。执行流程:

  1. x++ 返回 1,x 变为 2
  2. return 值(1)已缓存
  3. finally 中 x++,x 变为 3(但不影响返回值)
  4. 返回缓存的值 1

Q6: 如何设计一个良好的异常处理体系?

A:

  1. 分层设计:DAO 层抛出 SQLException,Service 层转换为业务异常
  2. 异常分类:系统异常、业务异常、第三方异常
  3. 统一处理:使用框架(如 Spring 的 @ExceptionHandler)统一处理
  4. 日志记录:异常发生时记录完整日志
  5. 用户友好:向前端返回友好的错误信息
// 分层异常处理示例
// DAO 层
public User findById(Long id) throws DataAccessException {
    try {
        // 数据库操作
    } catch (SQLException e) {
        throw new DataAccessException("查询失败", e);
    }
}
 
// Service 层
public User getUser(Long id) throws BusinessException {
    try {
        return userDao.findById(id);
    } catch (DataAccessException e) {
        throw new BusinessException("获取用户信息失败", e);
    }
}
 
// Controller 层(统一处理)
@ExceptionHandler(BusinessException.class)
public Result handleBusinessException(BusinessException e) {
    return Result.fail(e.getMessage());
}

Q7: 为什么不推荐用异常做流程控制?

A:

  1. 性能差:异常创建开销大,需要填充堆栈信息
  2. 可读性低:代码难以理解
  3. 难以维护:正常流程和异常处理混在一起
// ❌ 不推荐
public boolean isValid(String s) {
    try {
        Integer.parseInt(s);
        return true;
    } catch (NumberFormatException e) {
        return false;
    }
}
 
// ✅ 推荐
public boolean isValid(String s) {
    if (s == null) return false;
    return s.matches("\\d+");
}

八、总结

概念核心要点面试关键词
异常体系Throwable → Error/Exception → Checked/Unchecked继承关系、分类
try-catch-finally捕获异常、finally 必执行执行顺序、return 覆盖
throw vs throws抛出对象 vs 声明类型位置、作用不同
自定义异常继承 Exception 或 RuntimeException命名、构造器、异常链
最佳实践早抛出、晚捕获、保留异常链不要吞掉异常

最后更新:2026年3月2日