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

    • 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-with-resources

本节定位:掌握 JDK 7 引入的 try-with-resources 语法,理解 AutoCloseable 接口机制,以及它与传统 finally 关闭资源方式的本质区别。深入理解资源关闭顺序和异常抑制(suppressed exception)机制。


场景引入

"大翔,我的代码审查又被白歌打回来了。"小崔有点沮丧,"他说我的资源关闭代码写得像俄罗斯套娃,try 套 try,根本读不懂。"

大翔凑过去看小崔的屏幕:

// 小崔的代码:JDK 6 风格,三层嵌套关闭
public void exportStudentReport(String filePath, String query) {
    Connection conn = null;
    PreparedStatement ps = null;
    ResultSet rs = null;
    FileWriter writer = null;
    try {
        conn = DataSource.getConnection();
        ps = conn.prepareStatement(query);
        rs = ps.executeQuery();
        writer = new FileWriter(filePath);
        // 业务逻辑...
    } catch (SQLException | IOException e) {
        logger.error("导出失败", e);
    } finally {
        // 俄罗斯套娃式关闭
        if (rs != null) {
            try { rs.close(); } catch (SQLException e) { logger.warn("关闭失败", e); }
        }
        if (ps != null) {
            try { ps.close(); } catch (SQLException e) { logger.warn("关闭失败", e); }
        }
        if (conn != null) {
            try { conn.close(); } catch (SQLException e) { logger.warn("关闭失败", e); }
        }
        if (writer != null) {
            try { writer.close(); } catch (IOException e) { logger.warn("关闭失败", e); }
        }
    }
}

"JDK 7 就有的 try-with-resources 你竟然不用?"大翔笑了,"同样的代码,三行搞定。"


try-with-resources 语法

基本形式

try (资源声明; 资源声明; ...) {
    // 使用资源
} catch (异常类型 e) {
    // 处理异常
}
// 资源自动关闭,不需要 finally!
// JDK 7+:try-with-resources
public void exportStudentReport(String filePath, String query) {
    try (Connection conn = DataSource.getConnection();
         PreparedStatement ps = conn.prepareStatement(query);
         ResultSet rs = ps.executeQuery();
         FileWriter writer = new FileWriter(filePath)) {

        // 业务逻辑:直接使用资源,try 块结束时自动关闭
        while (rs.next()) {
            writer.write(rs.getString("name") + "," + rs.getInt("score") + "\n");
        }

    } catch (SQLException | IOException e) {
        logger.error("导出失败", e);
    }
    // finally 不需要了!资源会自动关闭
}

语法要点

要点说明
资源声明在 try() 中括号内声明的资源,try 块结束时会自动关闭
多个资源用分号分隔try (A a = ...; B b = ...; C c = ...)
资源必须实现 AutoCloseableJDK 7+:java.lang.AutoCloseable
资源隐式为 final不能在 try 块内重新赋值给资源变量
catch 和 finally 可选可以有 catch、finally、两者都有,或都没有

AutoCloseable 与 Closeable

接口对比

维度AutoCloseable (JDK 7)Closeable (JDK 5)
所在包java.langjava.io
close() 异常throws Exceptionthrows IOException
设计用途所有可关闭资源(通用)I/O 资源专用
幂等性要求无要求建议幂等(多次调用副作用相同)
继承关系父接口子接口(extends AutoCloseable)

为什么 Closeable 的 close() 抛 IOException 却能继承 AutoCloseable(close() 抛 Exception)? 因为 Java 中子类重写方法可以声明更具体的异常类型(协变返回类型的同理),这是合法的重写。


自动资源关闭的执行顺序

关键规则:后进先出(LIFO)

资源的关闭顺序与声明顺序相反——后声明的先关闭。

验证代码:

public class CloseOrderDemo {

    // 自定义资源类,实现 AutoCloseable,打印关闭日志
    static class MyResource implements AutoCloseable {
        private final String name;

        MyResource(String name) {
            this.name = name;
            System.out.println("打开资源: " + name);
        }

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

    public static void main(String[] args) {
        System.out.println("=== try-with-resources 关闭顺序 ===");

        try (MyResource r1 = new MyResource("Connection");
             MyResource r2 = new MyResource("PreparedStatement");
             MyResource r3 = new MyResource("ResultSet")) {

            System.out.println("--- 使用资源中 ---");

        } // try 块结束,自动关闭

        System.out.println("=== 程序结束 ===");
    }
}
运行输出:
=== try-with-resources 关闭顺序 ===
打开资源: Connection
打开资源: PreparedStatement
打开资源: ResultSet
--- 使用资源中 ---
关闭资源: ResultSet
关闭资源: PreparedStatement
关闭资源: Connection
=== 程序结束 ===

关闭顺序规律:后打开的依赖先打开的资源,所以必须先关闭依赖者。ResultSet 依赖 Statement,Statement 依赖 Connection(通过依赖关系自然形成 LIFO)。


异常抑制机制

什么是异常抑制

当一个异常在 try 块中被抛出,同时在自动关闭资源时 close() 方法也抛出了异常,JDK 的处理机制是:

  • try 块中的异常是"主异常"(primary exception)
  • close() 抛出的异常被"抑制"(suppressed exception),添加到主异常的 suppressed 列表中

验证代码

public class SuppressedExceptionDemo {

    // 一个在 close() 时也会抛异常的资源
    static class FaultyResource implements AutoCloseable {
        private final String name;

        FaultyResource(String name) {
            this.name = name;
        }

        @Override
        public void close() throws Exception {
            throw new Exception(name + " close() 异常");
        }

        public void use() throws Exception {
            throw new Exception(name + " 业务异常");
        }
    }

    public static void main(String[] args) {
        System.out.println("=== 异常抑制演示 ===\n");

        // 场景:try 块和 close() 都抛异常
        try (FaultyResource res = new FaultyResource("数据库连接")) {
            res.use();  // 业务异常(主异常)
        } catch (Exception e) {
            System.out.println("捕获到主异常: " + e.getMessage());

            // 获取被抑制的异常
            Throwable[] suppressed = e.getSuppressed();
            System.out.println("抑制的异常数量: " + suppressed.length);
            for (Throwable t : suppressed) {
                System.out.println("  抑制异常: " + t.getMessage());
            }
        }

        System.out.println("\n=== 对比:传统 finally 方式 ===");

        // 传统方式:close() 的异常会覆盖业务异常
        FaultyResource res2 = new FaultyResource("数据库连接");
        try {
            res2.use();  // 业务异常
        } catch (Exception e) {
            System.out.println("捕获异常: " + e.getMessage());
            // 这里只能捕获到业务异常,如果再 close 就覆盖了
        } finally {
            try {
                res2.close();  // close() 异常覆盖了业务异常!
            } catch (Exception e) {
                System.out.println("finally 中 close 异常覆盖了原始异常: " + e.getMessage());
            }
        }
    }
}
运行输出:
=== 异常抑制演示 ===

捕获到主异常: 数据库连接 业务异常
抑制的异常数量: 1
  抑制异常: 数据库连接 close() 异常

=== 对比:传统 finally 方式 ===
捕获异常: 数据库连接 业务异常
finally 中 close 异常覆盖了原始异常: 数据库连接 close() 异常

关键优势:try-with-resources 的异常抑制机制确保业务异常不会丢失,而传统 finally 方式中 close() 的异常会覆盖业务异常——这是 try-with-resources 相比手动关闭的重大优势。


完整演进:从 JDK 6 到 JDK 7+

JDK 6 风格:手动关闭(冗长且易错)

public String readStudentData(String filePath) {
    FileInputStream fis = null;
    try {
        fis = new FileInputStream(filePath);
        byte[] buffer = new byte[1024];
        int len = fis.read(buffer);
        return new String(buffer, 0, len);
    } catch (IOException e) {
        logger.error("读取失败: {}", filePath, e);
        return null;
    } finally {
        if (fis != null) {
            try {
                fis.close();
            } catch (IOException e) {
                logger.warn("关闭流失败", e);
            }
        }
    }
}

JDK 7 风格:try-with-resources(简洁安全)

public String readStudentData(String filePath) {
    try (FileInputStream fis = new FileInputStream(filePath)) {
        byte[] buffer = new byte[1024];
        int len = fis.read(buffer);
        return new String(buffer, 0, len);
    } catch (IOException e) {
        logger.error("读取失败: {}", filePath, e);
        return null;
    }
    // fis 自动关闭,无需 finally
}

JDK 9 增强:支持已存在的 effectively-final 变量

// JDK 9+(超出本章范围,但值得了解)
FileInputStream fis = new FileInputStream(filePath);
try (fis) {  // fis 是 effectively-final,可以直接使用
    byte[] buffer = new byte[1024];
    int len = fis.read(buffer);
    return new String(buffer, 0, len);
}

飞翔科技实战:学生数据导出的资源管理

场景

小崔开发的学生数据导出功能需要同时使用数据库连接、文件输出流、以及日志流。架构师白歌要求全部使用 try-with-resources 管理。

public class StudentDataExporter {

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

    /**
     * 导出指定班级的学生成绩报表
     *
     * @param classId   班级 ID
     * @param outputPath 输出文件路径
     * @return 导出的学生数量
     */
    public int exportScoreReport(String classId, String outputPath)
            throws ExportException {

        String sql = "SELECT s.student_id, s.name, sc.score " +
                     "FROM students s " +
                     "JOIN scores sc ON s.student_id = sc.student_id " +
                     "WHERE s.class_id = ? " +
                     "ORDER BY sc.score DESC";

        // 多个资源声明在 try() 中,分号分隔
        try (Connection conn = DataSourceManager.getConnection();
             PreparedStatement ps = conn.prepareStatement(sql);
             ResultSet rs = executeQuery(ps, classId);           // 注意顺序
             FileWriter writer = new FileWriter(outputPath);
             BufferedWriter bw = new BufferedWriter(writer)) {

            // 写入 CSV 头部
            bw.write("学号,姓名,成绩,排名");
            bw.newLine();

            int rank = 0;
            int count = 0;

            while (rs.next()) {
                rank++;
                count++;
                String line = String.format("%s,%s,%d,%d",
                    rs.getString("student_id"),
                    rs.getString("name"),
                    rs.getInt("score"),
                    rank);
                bw.write(line);
                bw.newLine();
            }

            logger.info("成绩报表导出成功: {}, 共 {} 名学生", outputPath, count);
            return count;

        } catch (SQLException e) {
            logger.error("数据库查询失败: classId={}", classId, e);
            throw new ExportException("无法查询班级 " + classId + " 的成绩数据", e);
        } catch (IOException e) {
            logger.error("文件写入失败: {}", outputPath, e);
            throw new ExportException("无法写入报表文件 " + outputPath, e);
        }
        // 所有资源按照 rs -> ps -> conn -> bw -> writer 的顺序自动关闭
    }

    private ResultSet executeQuery(PreparedStatement ps, String classId)
            throws SQLException {
        ps.setString(1, classId);
        return ps.executeQuery();
    }
}
运行输出(成功):
2024-01-15 14:00:00 INFO  c.f.s.exporter.StudentDataExporter - 成绩报表导出成功: /reports/class_CS2024.csv, 共 45 名学生

运行输出(数据库异常):
2024-01-15 14:01:00 ERROR c.f.s.exporter.StudentDataExporter - 数据库查询失败: classId=CS2024
com.feixiang.exception.ExportException: 无法查询班级 CS2024 的成绩数据
    at com.feixiang.student.exporter.StudentDataExporter.exportScoreReport(...)
Caused by: java.sql.SQLException: Communications link failure
    ...

易错场景

反例一:在 try 块中重新赋值资源变量

// ❌ 编译错误!try-with-resources 中声明的资源是隐式 final 的
try (FileWriter writer = new FileWriter("output.txt")) {
    writer = new FileWriter("another.txt");  // 编译错误!
    // 错误信息: auto-closeable resource writer may not be assigned
}
// ✅ 正确:不要在 try 块中重新赋值
try (FileWriter writer = new FileWriter("output.txt")) {
    writer.write("内容");
    // writer 不变
}

反例二:资源声明顺序错误导致依赖问题

// ❌ 危险:Connection 可能未初始化就被使用
try (PreparedStatement ps = conn.prepareStatement(sql);  // conn 在哪?
     Connection conn = DataSource.getConnection()) {
    // 编译错误!conn 还未声明
}
// ✅ 正确:被依赖的资源先声明
try (Connection conn = DataSource.getConnection();
     PreparedStatement ps = conn.prepareStatement(sql);
     ResultSet rs = ps.executeQuery()) {
    // 顺序:conn -> ps -> rs
}

反例三:JDK 6 资源类不能用于 try-with-resources

// ❌ 编译错误!MyOldResource 没有实现 AutoCloseable
class MyOldResource {  // JDK 6 的旧类
    public void close() {
        System.out.println("关闭");
    }
}

try (MyOldResource res = new MyOldResource()) {  // 编译错误!
    // 错误信息: incompatible types: try-with-resources not applicable
    //           to MyOldResource (MyOldResource does not implement AutoCloseable)
}
// ✅ 解决方案:让旧类实现 AutoCloseable
class MyUpdatedResource implements AutoCloseable {
    @Override
    public void close() {
        System.out.println("关闭");
    }
}

try (MyUpdatedResource res = new MyUpdatedResource()) {  // 通过
    // 使用资源
}

反例四:忘记 try-with-resources 不支持的条件

// ❌ 错误:在 try() 外面声明,里面赋值
Connection conn;
try (conn = DataSource.getConnection()) {  // 编译错误!
    // 错误信息: Resource declaration not allowed here
    //           since source level is below 9
}
// ✅ JDK 7/8 正确:在 try() 中声明并初始化
try (Connection conn = DataSource.getConnection()) {
    // 使用 conn
}

// ✅ JDK 9+ 支持外部 effectively-final 变量(但本章仅限 JDK 8)
// Connection conn = DataSource.getConnection();
// try (conn) { ... }

面试考点

Q1:try-with-resources 的底层实现原理是什么?

编译器在编译时将 try-with-resources 展开为传统的 try-catch-finally 结构。对于每个资源声明,编译器生成:① close() 调用放在隐式的 finally 块中;② 如果 try 块中抛出了异常,而 close() 也抛出了异常,使用 addSuppressed() 将 close() 的异常添加到主异常的 suppressed 列表中;③ 资源的关闭顺序与声明顺序相反(LIFO)。这些逻辑在编译后的字节码中是可以看到的。

Q2:AutoCloseable 和 Closeable 有什么区别?

AutoCloseable(JDK 7)是 java.lang 包中的接口,close() 方法声明为 throws Exception,是所有可自动关闭资源的根接口。Closeable(JDK 5)是 java.io 包中的接口,继承自 AutoCloseable,close() 方法声明为 throws IOException,专门用于 I/O 资源。JDK 7 之后 Closeable 继承了 AutoCloseable,所以所有 Closeable 的实现类都可以用于 try-with-resources。核心区别:AutoCloseable 更通用,Closeable 更专用。

Q3:什么是异常抑制(suppressed exception)?为什么需要它?

当 try-with-resources 中 try 块抛出一个异常(主异常),而在自动关闭资源时 close() 也抛出异常,后者的异常不会覆盖前者,而是被添加到主异常的 suppressed(抑制)列表中。通过 getSuppressed() 获取。这个机制解决了传统 finally 方式中 close() 异常覆盖业务异常的问题,确保真正的业务错误不会丢失。在日志中,被抑制的异常以 Suppressed: 前缀显示。

Q4:多个资源的关闭顺序是什么?为什么要这样设计?

关闭顺序是后进先出(LIFO)——后声明的资源先关闭。原因是资源之间存在依赖关系:ResultSet 依赖 Statement,Statement 依赖 Connection;BufferedWriter 依赖 FileWriter。后声明的资源往往依赖先声明的资源,所以必须逆序关闭,否则先关闭底层资源会导致上层资源无法正常 flush/close。

Q5:try-with-resources 中 catch 和 finally 块如何与自动关闭交互?

执行顺序是:① try 块执行;② 如果 try 块抛异常,跳转到 catch(如果匹配);③ 在任何情况下(正常结束或异常),隐式 finally 都会先自动关闭资源;④ 显式的 catch 在资源关闭后执行;⑤ 显式的 finally 在资源关闭和 catch 之后执行。关键点:资源关闭发生在 catch 和 finally 之前,这意味着在 catch 块中看到的被抑制异常已经通过 addSuppressed() 附加好了。

上一页
try-catch-finally
下一页
throws 与 throw