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 = ...) |
| 资源必须实现 AutoCloseable | JDK 7+:java.lang.AutoCloseable |
| 资源隐式为 final | 不能在 try 块内重新赋值给资源变量 |
| catch 和 finally 可选 | 可以有 catch、finally、两者都有,或都没有 |
AutoCloseable 与 Closeable
接口对比
| 维度 | AutoCloseable (JDK 7) | Closeable (JDK 5) |
|---|---|---|
| 所在包 | java.lang | java.io |
| close() 异常 | throws Exception | throws 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()附加好了。