目录

前言

一、异常继承体系

二、Checked Exception vs Unchecked Exception

2.1 Checked Exception(编译时异常)

2.2 Unchecked Exception(运行时异常)

2.3 对比总结

三、try-catch-finally 执行顺序

3.1 正常流程

3.2 经典坑:finally 中的 return ⭐⭐⭐

3.3 多 catch 的顺序

四、throw 与 throws

五、try-with-resources

5.1 传统写法的冗余

5.2 try-with-resources 语法

5.3 原理

5.4 多资源关闭顺序

六、自定义异常

七、异常处理最佳实践

八、面试高频题总结

参考


前言

异常处理看起来简单,但面试中经常出现"诡异"的执行顺序题。本文从异常继承体系出发,把 Checked/Unchecked 的区别、finally 的坑、以及 try-with-resources 的语法糖讲清楚。


一、异常继承体系

Throwable
├── Error(程序无法处理,JVM 层面的问题)
│   ├── OutOfMemoryError
│   ├── StackOverflowError
│   └── ...
└── Exception
    ├── RuntimeException(Unchecked,运行时异常)
    │   ├── NullPointerException
    │   ├── ArrayIndexOutOfBoundsException
    │   ├── ClassCastException
    │   ├── IllegalArgumentException
    │   └── ...
    └── 非 RuntimeException(Checked,编译时异常)
        ├── IOException
        ├── SQLException
        ├── FileNotFoundException
        └── ...

关键分界线:是否继承 RuntimeException


二、Checked Exception vs Unchecked Exception

2.1 Checked Exception(编译时异常)

编译器强制要求你处理,不处理就编译不过。

// 必须 try-catch 或 throws,否则编译报错
public void readFile() {
    FileInputStream fis = new FileInputStream("test.txt");  // IOException 是 Checked Exception
}

常见的 Checked Exception:

  • IOException / FileNotFoundException
  • SQLException
  • ClassNotFoundException
  • InterruptedException

2.2 Unchecked Exception(运行时异常)

编译器不强制处理,运行时才暴露。

// 编译能过,运行时才崩
public void demo() {
    String s = null;
    s.length();  // NullPointerException,运行时才报
}

常见的 Unchecked Exception:

  • NullPointerException ⭐ 最常见
  • ArrayIndexOutOfBoundsException
  • ClassCastException
  • ArithmeticException(除以零)
  • IllegalArgumentException
  • ConcurrentModificationException

2.3 对比总结

对比项Checked ExceptionUnchecked Exception
继承关系Exception 的子类(非 RuntimeException)RuntimeException 的子类
编译器检查强制处理(try-catch 或 throws)不强制
典型场景外部资源异常(IO、网络、数据库)代码逻辑错误(空指针、越界)
是否可恢复通常可恢复通常不可恢复(代码 bug)
是否应该捕获应该应该预防而非捕获

面试题ErrorException 的区别?
答:Error 是程序无法处理的严重错误(如 OOM),发生后程序应该直接退出。Exception 是程序可以处理的异常,应该被捕获和恢复。


三、try-catch-finally 执行顺序

3.1 正常流程

try {
    System.out.println("1. try");
    int result = 10 / 2;
} catch (ArithmeticException e) {
    System.out.println("2. catch");
} finally {
    System.out.println("3. finally");
}
// 输出:1. try → 3. finally

有异常时:

try {
    System.out.println("1. try");
    int result = 10 / 0;
} catch (ArithmeticException e) {
    System.out.println("2. catch");
} finally {
    System.out.println("3. finally");
}
// 输出:1. try → 2. catch → 3. finally

结论:finally 永远会执行(除了 System.exit() 或 JVM 崩溃的情况)。

3.2 经典坑:finally 中的 return ⭐⭐⭐

public static int test() {
    int x = 1;
    try {
        x++;
        return x;          // ① 准备返回 x=2
    } finally {
        x++;               // ② x 变成 3
        return x;          // ③ finally 的 return 覆盖了 try 的 return
    }
}

// 返回值:3(不是 2!)

为什么?finallyreturn覆盖 trycatch 中的 return

更坑的版本:

public static int test() {
    int x = 1;
    try {
        x++;
        return x;          // ① 准备返回 x=2,先把 2 存起来
    } finally {
        x++;               // ② x 变成 3,但存起来的返回值还是 2
    }
}

// 返回值:2(不是 3!)

这个更难理解:tryreturn x 时,JVM 会先把返回值暂存,然后执行 finally,最后返回暂存的值。finally 中修改 x 不影响已暂存的返回值。

面试题:try 中 return 后,finally 还会执行吗?
答:会。finally 在 return 之前执行。但如果 finally 也有 return,会覆盖 try 的 return。

3.3 多 catch 的顺序

try {
    // ...
} catch (Exception e) {
    // ...
} catch (IOException e) {    // ❌ 编译错误!IOException 是 Exception 的子类,永远不会走到
    // ...
}

规则:子类异常必须写在父类异常前面,否则编译报错。


四、throw 与 throws

// throws:声明方法可能抛出的异常(方法签名上)
public void readFile() throws IOException {
    // ...
}

// throw:手动抛出一个异常(方法体内)
public void setAge(int age) {
    if (age < 0) {
        throw new IllegalArgumentException("年龄不能为负数");
    }
}
对比项throwthrows
位置方法体内方法签名
作用抛出一个异常实例声明可能抛出的异常类型
数量一次只能抛一个可以声明多个

五、try-with-resources

5.1 传统写法的冗余

FileInputStream fis = null;
try {
    fis = new FileInputStream("test.txt");
    // 使用 fis
} catch (IOException e) {
    e.printStackTrace();
} finally {
    if (fis != null) {
        try {
            fis.close();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

finally 里又套了一层 try-catch,非常丑陋。

5.2 try-with-resources 语法

try (FileInputStream fis = new FileInputStream("test.txt")) {
    // 使用 fis
} catch (IOException e) {
    e.printStackTrace();
}
// fis 会自动关闭,不需要 finally

5.3 原理

try-with-resources 是语法糖,编译器会自动帮你生成 finally 中的 close() 调用。

前提是资源类必须实现 AutoCloseable 接口(Closeable 继承了 AutoCloseable)。

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

    @Override
    public void close() {
        System.out.println("资源关闭");
    }
}

// 使用
try (MyResource resource = new MyResource()) {
    resource.doSomething();
}
// 输出:工作 → 资源关闭

5.4 多资源关闭顺序

try (FileInputStream fis = new FileInputStream("in.txt");
     FileOutputStream fos = new FileOutputStream("out.txt")) {
    // ...
}
// 关闭顺序:先 fos,后 fis(与声明顺序相反,LIFO)

面试题:try-with-resources 中如果 try 和 close 都抛异常,哪个会被捕获?
答:try 中的异常会被捕获,close 中的异常会被抑制(suppressed exception),可以通过 getSuppressed() 获取。


六、自定义异常

// 自定义 Checked Exception
public class BusinessException extends Exception {
    private int code;

    public BusinessException(int code, String message) {
        super(message);
        this.code = code;
    }

    public int getCode() {
        return code;
    }
}

// 自定义 Unchecked Exception
public class ValidationException extends RuntimeException {
    public ValidationException(String message) {
        super(message);
    }
}

实际开发中通常继承 RuntimeException,因为 Checked Exception 会让代码充满 try-catch,影响可读性。


七、异常处理最佳实践

  1. 不要用异常做流程控制——异常应该处理真正的异常情况
  2. 不要捕获 ExceptionThrowable——应该精确捕获具体异常
  3. 不要忽略异常——空的 catch 块是定时炸弹
  4. 尽早抛出,尽晚捕获——在能处理的地方才捕获
  5. 使用 try-with-resources——自动关闭资源,避免资源泄漏
// ❌ 反面教材
try {
    // 一大堆代码
} catch (Exception e) {
    // 什么都不做
}

// ✅ 正确做法
try {
    // 尽量小的代码块
} catch (FileNotFoundException e) {
    log.warn("文件不存在: {}", filename);
    throw new BusinessException(404, "文件未找到");
}

八、面试高频题总结

问题核心答案
Error vs ExceptionError 不可恢复(OOM),Exception 可处理
Checked vs UncheckedChecked 编译时强制处理,Unchecked 运行时才暴露
finally 一定会执行吗会,除非 System.exit() 或 JVM 崩溃
finally 中 return 的坑会覆盖 try/catch 的 return,慎用
try-with-resources 原理语法糖,自动调用 close(),资源需实现 AutoCloseable
throw vs throwsthrow 抛异常实例,throws 声明异常类型

更多推荐