乐途乐途
主页
  • 计算机基础

    • TCP/IP
    • Linux
    • HTTP
  • 数据库

    • SQL
    • MySQL 5.7
  • 编程语言

    • C
    • C++
    • Java SE
    • Python2
    • Python3
  • 数据格式

    • JSON
    • XML
  • 认证与安全

    • JWT
  • 工具

    • Markdown
  • Git

    • GitFlow
  • Quartz

    • Quartz
  • Java

    • Maven 入门
    • Maven 进阶
    • MyBatis
    • Spring
    • Spring MVC
  • Java

    • Spring Boot
    • Spring Cloud
    • Spring Cloud Alibaba
    • Spring Security
    • Spring AI
    • Spring Batch
    • Kafka
    • Java 设计模式
  • 缓存

    • Redis
  • 搜索引擎

    • Elasticsearch
  • 分布式协调

    • ZooKeeper
联系
阿里云
主页
  • 计算机基础

    • TCP/IP
    • Linux
    • HTTP
  • 数据库

    • SQL
    • MySQL 5.7
  • 编程语言

    • C
    • C++
    • Java SE
    • Python2
    • Python3
  • 数据格式

    • JSON
    • XML
  • 认证与安全

    • JWT
  • 工具

    • Markdown
  • Git

    • GitFlow
  • Quartz

    • Quartz
  • Java

    • Maven 入门
    • Maven 进阶
    • MyBatis
    • Spring
    • Spring MVC
  • Java

    • Spring Boot
    • Spring Cloud
    • Spring Cloud Alibaba
    • Spring Security
    • Spring AI
    • Spring Batch
    • Kafka
    • Java 设计模式
  • 缓存

    • Redis
  • 搜索引擎

    • Elasticsearch
  • 分布式协调

    • ZooKeeper
联系
阿里云
  • 学习路径
  • 第1章 Java概述与环境搭建

    • Java概述与环境搭建
    • Java语言概述
    • 解释型语言与编译型语言对比
    • JDK安装与配置
    • JDK、JRE、JVM 详解
    • HelloWorld程序详解
    • IDE 介绍
  • 第2章 标识符与基本数据类型

    • 章节导读
    • 变量概述
    • 常量概述
    • 基本类型与包装类
    • 字节型 byte
    • 短整型 short
    • 整型 int
    • 长整型 long
    • 单精度浮点型 float
    • 双精度浮点型 double
    • 字符型 char
    • 布尔型 boolean
    • 类型转换
  • 第3章 运算符与表达式

    • 章节导读
    • 算术运算符
    • 赋值运算符
    • 关系运算符
    • 逻辑运算符
    • 位运算符
    • 条件运算符
    • 运算符优先级
    • 表达式
  • 第4章 流程控制

    • 章节导读
    • 常见的程序运行流程
    • if-else 选择结构
    • switch 多分支选择
    • while 循环
    • do-while 循环
    • for 循环
    • break 与 continue
  • 第5章 数组

    • 章节导读
    • 一维数组
    • 多维数组
    • Arrays 工具类
  • 第6章 类与对象

    • 章节导读
    • 类与对象
    • 方法定义与调用
    • 构造方法
    • 封装
    • 访问修饰符
    • package 与 import
    • static 关键字
    • this 关键字
    • 参数传递 详解
    • 枚举
    • 成员内部类
    • 局部内部类
    • 静态内部类
    • 匿名内部类
  • 第7章 接口与继承

    • 章节导读
    • 继承
    • super 关键字
    • final 关键字
    • 多态
    • 向上转型与向下转型
    • 抽象类
    • 接口
    • 抽象类与接口对比
  • 第8章 注解

    • 章节导读
    • 注解基础
    • 元注解详解
    • 自定义注解
  • 第9章 常用类

    • 章节导读:Java 常用类
    • Object 类:万类之祖
    • 包装类:基本类型的对象化
    • String:不可变的字符串
    • StringBuffer:线程安全的可变字符串
    • StringBuilder:可变的字符串构建器
    • Math:数学运算工具类
    • Random:伪随机数生成器
    • 大数值运算 详解
    • 日期时间API 详解
  • 第10章 异常机制

    • 章节导读
    • 异常体系与分类
    • try-catch-finally
    • try-with-resources
    • throws 与 throw
    • 自定义异常
  • 第11章 泛型

    • 章节导读
    • 泛型基础
    • 通配符与PECS原则
    • 类型擦除
  • 第12章 集合框架

    • 章节导读
    • 集合框架概述
    • ArrayList
    • LinkedList
    • HashMap 详解
    • LinkedHashMap 详解
    • TreeMap 详解
    • HashSet
    • TreeSet 详解
    • TreeSet 与 Comparable
    • Collections 工具类详解
  • 第13章 IO流

    • 章节导读
    • IO流概述
    • 字节流
    • 字符流
    • 缓冲流
    • 转换流 详解
    • 序列化 详解
    • NIO与Files 详解
    • NIO与Files工具类
  • 第14章 多线程与并发

    • 第十六章 多线程与并发 —— 章节导读
    • 线程基础详解
    • synchronized 详解
    • Lock 与显式锁详解
    • volatile 详解
    • wait 与 notify 详解
    • ThreadLocal详解
    • 原子类详解
    • 并发工具类详解
    • 线程池详解
  • 第15章 反射

    • 章节导读
    • 反射概述与 Class 对象
    • Constructor 与对象创建
    • Field与Method详解
    • 反射应用详解
  • 第16章 JDK8新特性

    • 章节导读
    • Lambda 表达式
    • Stream API 基础
    • Stream API 高级详解
    • Optional 详解
    • 新日期时间API详解
  • 第17章 JDK9-11新特性

    • 章节导读
    • 模块化系统 — Project Jigsaw(JDK 9)
    • var 局部变量类型推断(JDK 10)
    • 集合工厂方法与增强(JDK 9 / 10 / 11)
    • 接口增强:private 方法(JDK 9)
    • Stream API 增强(JDK 9)
    • Optional 增强(JDK 9 / 10 / 11)
    • String 新增方法(JDK 11)
    • HTTP Client 与 Files 增强(JDK 11)
    • 直接运行 Java 源文件 — JEP 330(JDK 11)
  • 第18章 JDK12-17新特性

    • 章节导读
    • Switch 表达式(JDK 12 预览 / JDK 14 正式)
    • 文本块 Text Blocks(JDK 13 预览 / JDK 15 正式)
    • Records 记录类(JDK 14 预览 / JDK 16 正式)
    • 密封类 Sealed Classes(JDK 15 预览 / JDK 17 正式)
    • instanceof 模式匹配(JDK 14 预览 / JDK 16 正式)
    • Switch 模式匹配 — Pattern Matching for switch(JDK 17 预览 / JDK 21 正式)
    • Helpful NPE 与 String 增强(JDK 12 / JDK 14 / JDK 15)
    • Stream 增强(JDK 12 / JDK 16)
    • 日期时间增强 — Day Period 支持(JDK 16)
  • 第19章 JDK18-21新特性

    • 章节导读
    • 虚拟线程(JDK 19 预览 / JDK 20 第二预览 / JDK 21 正式)
    • 序列集合(JDK 21 正式)
    • Switch 模式匹配(JDK 17 预览 / JDK 18 第二预览 / JDK 20 第四预览 / JDK 21 正式)
    • Record 模式匹配(JDK 19 预览 / JDK 20 第二预览 / JDK 21 正式)
    • 未命名模式与变量(JDK 21 预览 / JDK 22 正式)
  • 第20章 JDK 22-25 新特性

    • 章节导读
    • 字符串模板(JDK 22 预览 / JDK 23 第二预览 / JDK 24 第三预览)
    • Stream Gatherers(JDK 22 预览 / JDK 24 第二预览)
    • 隐式声明类与实例方法(JDK 23 预览 / JDK 24 第二预览)
    • 原始类型模式匹配(JDK 24 预览)
  • 附录

    • Java 核心知识点
    • Java SE 专业术语
    • Java特性索引(JDK 8 → 25)

try-catch-finally

本节定位:深入理解 try-catch-finally 的语法规则、执行流程和底层实现。掌握多个 catch 块的匹配顺序、finally 的执行时机,以及 finally 中 return 的经典陷阱。从字节码异常表的角度理解这一切是如何运作的。


场景引入

飞翔科技的实习生李眉写了一段文件读取代码,自认为万无一失:

public String readFirstLine(String path) {
    try {
        BufferedReader reader = new BufferedReader(new FileReader(path));
        return reader.readLine();
    } catch (Exception e) {
        System.out.println("出错了");
    } finally {
        System.out.println("清理资源");
    }
    return null;
}

大翔审查后指出了三个问题:

  1. catch 块吞掉了异常,调用方完全不知道发生了什么
  2. reader 没有关闭,如果文件很大,会导致文件句柄泄漏
  3. catch (Exception) 抓了太多不该抓的东西

"try-catch-finally 看起来简单,"大翔说,"但魔鬼都在细节里。"


基础语法

try-catch 语法

try {
    // 可能抛出异常的代码块
    // ...
} catch (ExceptionType1 e) {
    // 处理 ExceptionType1 及其子类的异常
} catch (ExceptionType2 e) {
    // 处理 ExceptionType2 及其子类的异常
}
// 后续代码
组成部分说明是否必需
try 块包含可能抛出异常的代码必需
catch 块捕获并处理指定类型的异常至少一个(或 finally)
finally 块无论是否异常都会执行的代码可选

约束规则:

规则说明
try 不能单独存在必须至少跟随一个 catch 或一个 finally
catch 不能单独存在必须跟在 try 之后
多 catch 顺序子类异常必须在父类异常之前
finally 最多一个如果有,必须在所有 catch 之后

catch 块的匹配机制

从上到下、精确匹配

JVM 在 catch 块中按照书写顺序从上到下匹配异常类型。一旦匹配成功,后续的 catch 块不再执行。

try {
    FileReader reader = new FileReader("students.txt");
} catch (FileNotFoundException e) {    // ① 先匹配:子类 FileNotFoundException
    System.out.println("文件未找到");
} catch (IOException e) {              // ② 后匹配:父类 IOException
    System.out.println("IO 异常");
}

匹配流程:

多 catch 块顺序的铁律:子类在前,父类在后

// ❌ 编译错误!父类在前,后面的子类 catch 永远不会执行
try {
    FileReader reader = new FileReader("test.txt");
} catch (IOException e) {              // 父类在前
    System.out.println("IO 异常");
} catch (FileNotFoundException e) {    // 编译错误:unreachable catch block
    System.out.println("文件未找到");   // 永远不会被执行!
}
// ✅ 正确:子类在前
try {
    FileReader reader = new FileReader("test.txt");
} catch (FileNotFoundException e) {    // 子类在前
    System.out.println("文件未找到");
} catch (IOException e) {              // 父类在后
    System.out.println("IO 异常");
}

JDK 7+ 多异常捕获语法:如果多个 catch 块的处理逻辑相同,可以用 | 合并:

catch (FileNotFoundException | SQLException e) {
    System.out.println("文件或数据库异常: " + e.getMessage());
}
// 注意:合并的异常类型不能有父子关系,且 e 隐式为 final

finally 块:不可靠世界中的可靠保证

核心规则

finally 块中的代码无论是否发生异常都会执行(除非 JVM 崩溃或 System.exit())。

public int testFinally() {
    try {
        System.out.println("1. try 块");
        int x = 10 / 0;           // 抛出 ArithmeticException
        return 100;               // 这行不会执行
    } catch (ArithmeticException e) {
        System.out.println("2. catch 块");
        return 200;               // 注意:return 前会先执行 finally
    } finally {
        System.out.println("3. finally 块");
    }
}

// 输出:
// 1. try 块
// 2. catch 块
// 3. finally 块
// 返回: 200

finally 执行的四种场景

场景try 中情况catch 中情况finally 是否执行
正常执行无异常不执行✅ 执行
捕获异常抛出异常捕获并处理✅ 执行
未捕获异常抛出异常未匹配✅ 执行(然后异常继续传播)
System.exit()调用 exit—❌ 不执行
returnreturn 语句—✅ 在 return 之前执行

深度原理:异常表的字节码实现

编译前后对比

让我们深入 JVM 层面,理解 try-catch-finally 是如何被编译成字节码的。

源代码:

public static int divide(int a, int b) {
    try {
        int result = a / b;
        return result;
    } catch (ArithmeticException e) {
        System.out.println("除数不能为零");
        return -1;
    } finally {
        System.out.println("计算结束");
    }
}

编译后的等价伪代码(展示 finally 的"复制"机制):

// JVM 将 finally 块代码复制到每个出口前
public static int divide(int a, int b) {
    try {
        int result = a / b;
        // --- finally 代码复制到正常 return 前 ---
        System.out.println("计算结束");
        return result;
    } catch (ArithmeticException e) {
        System.out.println("除数不能为零");
        // --- finally 代码复制到 catch 的 return 前 ---
        System.out.println("计算结束");
        return -1;
    } catch (Throwable t) {  // 隐式 catch:捕获未被匹配的异常
        // --- finally 代码复制到异常抛出前 ---
        System.out.println("计算结束");
        throw t;              // 重新抛出
    }
}

字节码异常表分析:

假设上述 divide 方法编译后的字节码偏移如下:

字节码偏移源码对应
0-5int result = a / b
6-8准备 return result
9-12System.out.println("除数不能为零")
13-15return -1
16-18System.out.println("计算结束") ← finally 块(被复制到多处)

生成的异常表(Exception Table):

fromtotargettype
059ArithmeticException
0516any
91216any

异常表解读:

  • 第1行:如果 0-5 偏移处抛出 ArithmeticException,跳转到 9(catch 块)
  • 第2行:如果 0-5 偏移处抛出任何其他异常,跳转到 16(finally 块)
  • 第3行:如果 catch 块自身抛出异常,跳转到 16(finally 块)
  • 这就是 finally "无论是否异常都能执行" 的字节码级保证

核心陷阱:finally 中的 return

陷阱代码

这是 Java 异常处理中最经典的面试题之一:

public static int test() {
    int x = 1;
    try {
        return x;       // ① 准备返回 1
    } finally {
        x = 2;          // ② 修改 x
        return x;       // ③ 返回 2!覆盖了 try 的返回值
    }
}

public static void main(String[] args) {
    System.out.println(test());  // 输出: 2
}

为什么会输出 2?

JVM 执行细节:

  1. return x 被拆分为两步:先计算返回值(将 x 的值 1 暂存到局部变量),再执行返回指令
  2. 在返回指令执行前,JVM 先执行 finally 块
  3. finally 中的 x = 2 修改了局部变量 x,但返回值已经暂存为 1
  4. 但是!finally 中又执行了 return x,这个 return 覆盖了之前的暂存返回值
  5. 最终方法返回 2
// 更详细的分析
public static int test2() {
    int x = 1;
    try {
        return x;       // 返回值暂存为 1,等待 finally 执行完后再返回
    } finally {
        x = 2;          // 修改 x,但不影响已暂存的返回值
        // 没有 return,使用 try 的暂存返回值
    }
    // 实际返回:1(finally 没有 return 时,返回暂存值)
}

public static int test3() {
    int x = 1;
    try {
        return x;       // 返回值暂存为 1
    } finally {
        x = 2;
        return x;       // 覆盖暂存值,返回 2
    }
}

完整验证代码

public class FinallyReturnTrap {

    public static int case1() {
        try {
            return 10;
        } finally {
            return 20;  // finally 中有 return
        }
    }

    public static int case2() {
        int x = 10;
        try {
            return x;
        } finally {
            x = 20;
            // finally 中没有 return
        }
    }

    public static String case3() {
        StringBuilder sb = new StringBuilder("hello");
        try {
            return sb.toString();      // 返回 "hello" 字符串(不可变)
        } finally {
            sb.append(" world");       // 修改 sb,但不影响已创建的字符串
        }
    }

    public static StringBuilder case4() {
        StringBuilder sb = new StringBuilder("hello");
        try {
            return sb;                 // 返回对象引用
        } finally {
            sb.append(" world");       // 修改了对象内容!返回的引用指向同一个对象
        }
    }

    public static void main(String[] args) {
        System.out.println("case1: " + case1());
        // 输出:case1: 20
        // 原因:finally 中的 return 覆盖了 try 的 return

        System.out.println("case2: " + case2());
        // 输出:case2: 10
        // 原因:finally 没有 return,使用 try 的暂存返回值

        System.out.println("case3: " + case3());
        // 输出:case3: hello
        // 原因:String 不可变,sb.toString() 已经创建了新字符串

        System.out.println("case4: " + case4());
        // 输出:case4: hello world
        // 原因:返回的是 StringBuilder 对象引用,finally 修改了对象内容
    }
}
运行输出:
case1: 20
case2: 10
case3: hello
case4: hello world

切记:永远不要在 finally 中写 return!它会让代码行为变得难以预测,且多数代码审查工具(如 SonarQube)会将此标记为严重问题。


飞翔科技实战:学生管理系统中的异常处理

场景一:学生成绩导入

小崔写了一个成绩导入方法,需要从文件读取成绩数据并写入数据库。

public class ScoreImporter {

    private static final Logger logger = LoggerFactory.getLogger(ScoreImporter.class);

    /**
     * 导入学生成绩
     * @param filePath 成绩数据文件路径
     * @return 成功导入的记录数
     */
    public int importScores(String filePath) {
        BufferedReader reader = null;
        Connection conn = null;
        int importedCount = 0;

        try {
            // ① 打开文件
            reader = new BufferedReader(new FileReader(filePath));
            // ② 获取数据库连接
            conn = DataSourceManager.getConnection();
            conn.setAutoCommit(false);  // 开启事务

            String line;
            while ((line = reader.readLine()) != null) {
                String[] parts = line.split(",");
                String studentId = parts[0];
                int score = Integer.parseInt(parts[1]);

                // ③ 插入成绩记录
                PreparedStatement ps = conn.prepareStatement(
                    "INSERT INTO scores (student_id, score) VALUES (?, ?)");
                ps.setString(1, studentId);
                ps.setInt(2, score);
                ps.executeUpdate();
                importedCount++;
            }

            // ④ 提交事务
            conn.commit();
            logger.info("成绩导入成功,共 {} 条记录", importedCount);

        } catch (FileNotFoundException e) {
            logger.error("成绩文件不存在: {}", filePath, e);
            // 提示用户重新选择文件
            throw new ImportException("文件不存在,请检查路径", e);
        } catch (IOException e) {
            logger.error("读取成绩文件失败", e);
            throw new ImportException("文件读取失败", e);
        } catch (SQLException e) {
            logger.error("数据库操作失败,已回滚", e);
            // 回滚事务
            if (conn != null) {
                try {
                    conn.rollback();
                } catch (SQLException ex) {
                    logger.error("回滚失败", ex);
                }
            }
            throw new ImportException("成绩数据保存失败", e);
        } catch (NumberFormatException e) {
            logger.error("成绩数据格式错误", e);
            throw new ImportException("文件包含非法数字格式", e);
        } finally {
            // ⑤ 关闭资源(严格按照打开顺序反向关闭)
            if (reader != null) {
                try {
                    reader.close();
                } catch (IOException e) {
                    logger.warn("关闭文件读取器失败", e);
                }
            }
            if (conn != null) {
                try {
                    conn.setAutoCommit(true);  // 恢复默认
                    conn.close();
                } catch (SQLException e) {
                    logger.warn("关闭数据库连接失败", e);
                }
            }
        }

        return importedCount;
    }
}
运行输出(正常情况):
2024-01-15 09:30:00 INFO  c.f.s.importer.ScoreImporter - 成绩导入成功,共 150 条记录

运行输出(文件不存在):
2024-01-15 09:31:00 ERROR c.f.s.importer.ScoreImporter - 成绩文件不存在: /data/scores.csv
com.feixiang.exception.ImportException: 文件不存在,请检查路径
    at com.feixiang.student.importer.ScoreImporter.importScores(ScoreImporter.java:45)
Caused by: java.io.FileNotFoundException: /data/scores.csv (系统找不到指定的路径。)
    at java.io.FileInputStream.open0(Native Method)
    at java.io.FileInputStream.open(FileInputStream.java:195)
    ...

场景二:finally 资源关闭的演进

// JDK 6 及之前:手动关闭,嵌套 try-catch,代码臃肿
public String readStudentName(String path) {
    FileInputStream fis = null;
    try {
        fis = new FileInputStream(path);
        byte[] data = new byte[1024];
        int len = fis.read(data);
        return new String(data, 0, len);
    } catch (IOException e) {
        logger.error("读取失败", e);
        return null;
    } finally {
        if (fis != null) {
            try {
                fis.close();  // close() 也抛异常!需要嵌套 try-catch
            } catch (IOException e) {
                logger.warn("关闭流失败", e);
            }
        }
    }
}

// JDK 7+:try-with-resources,自动关闭,简洁优雅
// (详见本章 try-with-resources 节)
public String readStudentName(String path) {
    try (FileInputStream fis = new FileInputStream(path)) {
        byte[] data = new byte[1024];
        int len = fis.read(data);
        return new String(data, 0, len);
    } catch (IOException e) {
        logger.error("读取失败", e);
        return null;
    }
}

易错场景

反例一:catch 块顺序错误

// ❌ 编译错误:IOException 在前,FileNotFoundException 永远匹配不到
try {
    Files.readAllLines(Paths.get("students.txt"));
} catch (IOException e) {           // 父类
    System.out.println("IO异常");
} catch (FileNotFoundException e) { // 子类,unreachable!
    System.out.println("文件未找到");
}

// 编译错误信息:
// error: exception FileNotFoundException has already been caught

反例二:捕获了异常但不处理(吞异常)

// ❌ 最糟糕的做法:吞掉异常
try {
    studentDao.save(student);
} catch (SQLException e) {
    // 什么都不做,异常被吞掉了!
    // 数据没有保存成功,但调用方完全不知情
}
// ✅ 正确做法
try {
    studentDao.save(student);
} catch (SQLException e) {
    // 至少记录日志
    logger.error("保存学生信息失败: {}", student.getId(), e);
    // 包装为业务异常重新抛出,让调用方知情
    throw new StudentServiceException("保存学生信息失败", e);
}

反例三:finally 中 return

// ❌ 经典陷阱:finally 中的 return 吞掉了异常
public int riskyMethod() {
    try {
        throw new RuntimeException("严重错误");
    } finally {
        return 0;  // 异常被吞掉了!调用方拿到 0,完全不知道出了异常
    }
}

// 调用方:
int result = riskyMethod();  // 返回 0,没有任何异常信息
System.out.println("结果:" + result);  // 结果:0(浑然不知出过错)
// ✅ 正确:finally 只做清理,不改变控制流
public int riskyMethod() throws RuntimeException {
    try {
        throw new RuntimeException("严重错误");
    } finally {
        // 只做清理,不 return、不 throw(除非万不得已)
        cleanupResources();
    }
}

反例四:在 finally 中抛出异常,覆盖原始异常

// ❌ 错误:finally 中的异常覆盖了原始异常
public void processFile(String path) throws Exception {
    FileInputStream fis = null;
    try {
        fis = new FileInputStream(path);
        // 假设这里抛出了 IOException
        int result = 10 / 0;  // ArithmeticException(原始异常)
    } finally {
        if (fis != null) {
            fis.close();  // close 也抛出了 IOException!
            // 原始 ArithmeticException 被 finally 的 IOException 覆盖了
        }
    }
}
// ✅ 正确:JDK 7+ try-with-resources 利用异常抑制(suppressed)机制
// 原始异常为主异常,close() 的异常被抑制
try (FileInputStream fis = new FileInputStream(path)) {
    int result = 10 / 0;  // ArithmeticException(主异常)
}
// close() 的 IOException 被添加到主异常的 suppressed 列表中

面试考点

Q1:finally 块中的代码一定会执行吗?

基本是的,但有例外:① System.exit() 被调用(JVM 进程终止);② 守护线程中所有非守护线程结束,JVM 退出;③ JVM 崩溃(如 OutOfMemoryError 后没有足够内存执行 finally);④ Runtime.halt() 强制终止。注意:return、break、continue 都不会阻止 finally 执行。

Q2:为什么 finally 中不应该写 return?

① finally 中的 return 会覆盖 try 或 catch 中的 return 值;② 会吞掉任何未被捕获的异常,让调用方误以为一切正常;③ 违反了 finally 的语义——finally 应该只做清理,不应该改变方法的控制流。多数静态分析工具将其标记为严重缺陷。

Q3:try-catch-finally 中,如果 try 中 return 了,finally 什么时候执行?

在 try 的 return 语句执行到一半时:JVM 先计算返回值并暂存,然后执行 finally 块,最后将暂存的返回值返回。如果 finally 中也有 return,则覆盖暂存值(这就是不应在 finally 中 return 的原因)。

Q4:多 catch 块的匹配顺序是什么?

按书写顺序从上到下匹配,匹配成功后不再检查后续 catch。因此子类异常 catch 必须写在父类之前,否则编译错误(unreachable catch block)。JDK 7+ 支持多异常合并 catch (A | B e),但 A 和 B 不能有继承关系。

Q5:异常被捕获后,调用栈是怎样的?

异常被 catch 后,调用栈仍然存在(可以通过 e.getStackTrace() 获取),只是异常不再向上传播。如果 catch 中重新 throw 了新的异常,原始异常可以通过异常链(new Exception("msg", originalException))关联,此时原始异常的栈信息保留在 getCause() 中。

上一页
异常体系与分类
下一页
try-with-resources