Java try-with-resources 实战:告别 finally 里手动关流的四个坑

发布时间:2026/7/18 9:14:26
Java try-with-resources 实战:告别 finally 里手动关流的四个坑 Java try-with-resources 实战:告别 finally 里手动关流的四个坑Java 里凡是「打开了要关」的东西——文件流、数据库连接、Socket、Lock——手动关闭都是 bug 高发区。经典的 finally 写法又长又容易写错:忘了关是资源泄漏,关的顺序错了或者关的时候又抛异常,还会把原始异常吞掉,排查起来一头雾水。try-with-resources 从 Java 7 就有了,但真正用对、避开它的几个坑的人不多。这篇把手动 finally 的问题、正确写法、以及四个容易踩的坑一次讲清楚。先看老写法有多容易错读一个文件,用传统 try-finally:publicStringreadFile(Stringpath)throwsIOException{BufferedReaderreadernull;try{readernewBufferedReader(newFileReader(path));returnreader.readLine();}finally{if(reader!null){// 必须判空,否则构造失败时这里 NPEreader.close();// close 自己也会抛 IOException}}}这段代码有两个隐患:finally里的reader.close()也可能抛异常。如果 try 块里readLine()抛了异常 A,close 又抛异常 B,那么方法最终抛出的是 B——原始异常 A 被彻底吞掉了。而你真正想知道的是 A(读取失败的原因),B(关闭失败)反而是次要的。这是 finally 关流最坑的地方。多个资源时,finally 会嵌套成金字塔,判空、关闭顺序全靠手写,极易出错。正确写法:try-with-resources把资源声明放进try(...)的括号里,Java 会在 try 块结束时自动、逆序关闭它们,不用写 finally:publicStringreadFile(Stringpath)throwsIOException{// 资源在括号里声明,块结束自动 closetry(BufferedReaderreadernewBufferedReader(newFileReader(path))){returnreader.readLine();}// 不需要 finally,reader 保证被关闭}代码短了一半,而且没有判空、没有手动 close。任何实现了AutoCloseable接口的类都能这么用——JDK 里的流、Connection、Statement、ResultSet全都实现了。多个资源用分号隔开,关闭顺序和声明顺序相反(后声明的先关,符合依赖关系):try(ConnectionconndataSource.getConnection();PreparedStatementpsconn.prepareStatement(SELECT * FROM users WHERE id ?);){ps.setLong(1,userId);try(ResultSetrsps.executeQuery()){if(rs.next()){returnrs.getString(name);}}}// 关闭顺序:rs - ps - conn,自动逆序,正好符合依赖坑一:自定义资源类要实现 AutoCloseabletry-with-resources 只对实现了AutoCloseable(或其子接口Closeable)的类生效。你自己封装的资源类想享受自动关闭,得实现这个接口:publicclassFileLockimplementsAutoCloseable{privatefinalStringname;publicFileLock(Stringname){this.namename;System.out.println(获取锁: name);}Overridepublicvoidclose(){// try 块结束时自动调用System.out.println(释放锁: name);}}// 使用try(FileLocklocknewFileLock(order)){// 业务逻辑,无论正常返回还是抛异常,close 都会执行doSomething();}Closeable.close()声明抛IOException,而AutoCloseable.close()抛的是Exception——如果你的 close 不抛受检异常,直接实现AutoCloseable更干净,别无脑用Closeable。坑二:被压制的异常去哪了(suppressed exception)前面说老写法会吞掉原始异常。try-with-resources 解决了这个问题,但解决方式你得知道:如果 try 块抛异常 A、close 时又抛异常 B,那么方法抛出的是 A(你关心的原始异常),B 被压制并挂在 A 上,不会丢失。publicclassResourceimplementsAutoCloseable{Overridepublicvoidclose(){thrownewRuntimeException(close 失败);// 异常 B}}try(ResourcernewResource()){thrownewRuntimeException(业务失败);// 异常 A}catch(Exceptione){System.out.println(主异常: e.getMessage());// 业务失败(A)for(Throwablesup:e.getSuppressed()){System.out.println(被压制: sup.getMessage());// close 失败(B)}}输出:主异常: 业务失败 被压制: close 失败对比老写法「A 被 B 覆盖、彻底看不到」,这里 A 是主异常、B 通过getSuppressed()仍能拿到。打日志时如果直接打印异常栈,现代日志框架会自动带上 “Suppressed:” 段,信息完整。这是 try-with-resources 比 finally 强的核心价值,不只是少写代码。坑三:资源变量在 try 块里不能重新赋值括号里声明的资源是隐式 final 的,不能在 try 块内重新赋值(否则关闭的可能不是你以为的那个对象)。Java 9 起还支持「引用已有的 final 变量」,不必非在括号里 new:// Java 9可以引用外部已存在的 final/effectively-final 变量BufferedReaderreadernewBufferedReader(newFileReader(a.txt));try(reader){// 直接放变量名,不用重新声明returnreader.readLine();}// reader 仍会被自动关闭// 但下面这样在 Java 8 里编译不过,必须在括号内声明并初始化:// try (reader new BufferedReader(...)) { } // 编译错误Java 8 只能在括号里声明新变量;Java 9 才允许引用外部 effectively-final 变量,写法更灵活。坑四:close 抛异常但你不想让它影响主流程有些场景 close 失败无所谓(比如只读的流关闭失败),你不想让被压制的异常干扰。这时可以在资源类的 close 里自己吞掉或降级为日志,而不是让它抛出去:publicclassQuietReaderimplementsAutoCloseable{privatefinalBufferedReaderdelegate;publicQuietReader(Stringpath)throwsIOException{this.delegatenewBufferedReader(newFileReader(path));}publicStringreadLine()throwsIOException{returndelegate.readLine();}Overridepublicvoidclose(){try{delegate.close();}catch(IOExceptione){// close 失败只记日志,不往上抛,避免污染主异常/被压制列表System.err.println(关闭失败,已忽略: e.getMessage());}}}要不要这么做取决于业务:写操作的 close 失败往往意味着数据没落盘,绝不能吞;纯读操作的 close 失败通常可以降级。别一刀切。小结手动finally关资源有两大硬伤:要判空、且 close 抛异常会吞掉原始异常,排查困难。try-with-resources 把资源放进try(...)括号,块结束自动逆序关闭,代码减半且不会吞异常。前提是资源类实现AutoCloseable;不抛受检异常就直接实现AutoCloseable,别用Closeable。异常处理是核心优势:try 块的异常是主异常,close 的异常被压制挂在上面,用getSuppressed()能拿到,信息不丢。Java 9 可以try(existingVar)引用外部 final 变量;资源变量在块内不可重新赋值。记忆点:凡是 implements AutoCloseable 的东西,一律用 try-with-resources,别再手写 finally 关流。