try-with-resources语句
try-with-resources语句是一种声明了一种或多种资源的try语句。资源是指在程序用完了之后必须要关闭的对象。try-with-resources语句保证了每个声明了的资源在语句结束的时候都会被关闭。任何实现了java.lang.AutoCloseable接口的对象,和实现了java.io.Closeable接口的对象,都可以当做资源使用。
新结构扩展了 try 块,按照与 for 循环相似的方式声明了资源。在 try 块中声明打开的任何资源都会关闭。因此,这个新结构使您不必配对使用 try 块与对应的 finally 块,后者专用于正确的资源管理。使用分号分隔每个资源
下面的例子读取了一个文件的第一行。它使用了一个BufferedReader实例去读取文件,BufferedReader是一种资源,用完之后必须关闭。
static String readFirstLineFromFile(String path) throws IOException {
try (BufferedReader br =
new BufferedReader(new FileReader(path))) {
return br.readLine();
}
}
在这个例子中,try-with-resources语句种声明的是BufferedReader资源。声明语句紧跟着在try关键词的圆括号里面。BufferedReader从Java SE7开始就实现了java.lang.AutoCloseable接口。因为BufferedReader声明在了try-with-resources里面,所以无论try语句是正常结束还是异常结束(比方说BufferedReader.readLine方法抛出一个IOException异常),它都会被关闭。
在Java SE7之前,你可以用finally代码块来确保资源一定被关闭,无论try语句正常结束还是异常结束。下面的例子用finally代码块代替try-with-resources语句:
static String readFirstLineFromFileWithFinallyBlock(String path)
throws IOException {
BufferedReader br = new BufferedReader(new FileReader(path));
try {
return br.readLine();
} finally {
if (br != null) br.close();
}
}
然而,在这个例子中,如果readLine和close方法都抛出异常,那么readFirstLineFromFileWithFinallyBlock方法最终会在finally代码块抛出异常,从try代码块里面抛出的那个异常被抑制住了。相比之下,readFirstLineFromFile方法中,如果try代码块和try-with-resources语句同时抛出异常,这个方法将会最终抛出try代码块里面的异常,try-with-resources语句里面抛出的异常被压抑了。在Java SE7之后,你可以找回被压抑的异常。参看后面的“被压抑的异常”部分。
try-with-resources语句里面声明多个资源
下面的例子展示了从一个名字为zipFileName的zip文件中检索出里面所有文件的名称,并创建一个文本文件存储所有文件名称。
public static void writeToFileZipFileContents(String zipFileName,
String outputFileName)
throws java.io.IOException {
java.nio.charset.Charset charset =
java.nio.charset.StandardCharsets.US_ASCII;
java.nio.file.Path outputFilePath =
java.nio.file.Paths.get(outputFileName);
// Open zip file and create output file with
// try-with-resources statement
try (
java.util.zip.ZipFile zf =
new java.util.zip.ZipFile(zipFileName);
java.io.BufferedWriter writer =
java.nio.file.Files.newBufferedWriter(outputFilePath, charset)
) {
// Enumerate each entry
for (java.util.Enumeration entries =
zf.entries(); entries.hasMoreElements();) {
// Get the entry name and write it to the output file
String newLine = System.getProperty("line.separator");
String zipEntryName =
((java.util.zip.ZipEntry)entries.nextElement()).getName() +
newLine;
writer.write(zipEntryName, 0, zipEntryName.length());
}
}
}
在这个例子中,try-with-resources语句包含了两个用分号隔开的声明:ZipFile和BufferedWriter。当代码块中代码终止,不管是正常还是异常,BufferedWriter和ZipFile对象的close方法都会自动按声明的相反顺序调用。
下面的例子用try-with-resources语句自动关闭一个java.sql.Statement对象:
public static void viewTable(Connection con) throws SQLException {
String query = "select COF_NAME, SUP_ID, PRICE, SALES, TOTAL from COFFEES";
try (Statement stmt = con.createStatement()) {
ResultSet rs = stmt.executeQuery(query);
while (rs.next()) {
String coffeeName = rs.getString("COF_NAME");
int supplierID = rs.getInt("SUP_ID");
float price = rs.getFloat("PRICE");
int sales = rs.getInt("SALES");
int total = rs.getInt("TOTAL");
System.out.println(coffeeName + ", " + supplierID + ", " +
price + ", " + sales + ", " + total);
}
} catch (SQLException e) {
JDBCTutorialUtilities.printSQLException(e);
}
}
例子中的资源java.sql.Statement是JDBC 4.1及其后面版本的一部分。
注意:try-with-resources语句也可以像普通的try语句一样,有catch和finally代码块。在try-with-resources语句中,任何的catch和finally代码块都在所有被声明的资源被关闭后执行。
被压抑的异常
try-with-resources语句相关联的代码块可能会抛出异常。在writeToFileZipFileContents例子中,try代码块中可能会抛出异常,并且有高达两个异常可能会在try-with-resources语句抛出,当试图去关闭ZipFile和BufferedWriter对象的时候。如果在try代码块中抛出一个异常,同时在try-with-resources语句中抛出一个或多个异常,那么在try-with-resources语句中抛出的异常就被压抑了,并且最终在writeToFileZipFileContents方法抛出的异常就是try代码块中抛出的那个异常。你可以通过被try代码块抛出的异常的Throwable.getSuppressed方法找回被压抑的异常。
实现了AutoCloseable或Closeable接口的类
看 AutoCloseable 和 Closeable 接口的javadoc,看看实现了它们的类都有哪些。Closeable接口继承了AutoCloseable接口。不同之处在于,Closeable接口的close方法抛出的是IOException异常,AutoCloseable接口的close方法抛出的是Exception异常。 因此,AutoCloseable的子类可以重写close方法的行为,抛出指定的异常,比如IOException异常,甚至可以不抛出异常。强烈建议,实现的类和子接口应声明一种比 java.lang.Exception 更精确的异常类型,当然,更好的情况是,如果调用 close() 方法不会导致失败,就根本不用声明异常类型。
深入理解try-with-resources语法
下面模拟实现资源调用work(),模拟自动关闭方法close(),然后分别主动抛出异常,来看看新语法是如何处理被抑制的异常的情况。
public class TestResources {
public static void main(String args[]) throws MyException {
try (AutoClose autoClose = new AutoClose()) {
autoClose.work();
}
}
}
public class AutoClose implements AutoCloseable {
@Override
public void close() {
System.out.println(">>> close()");
throw new RuntimeException("Exception in close()");
}
public void work() throws MyException {
System.out.println(">>> work()");
throw new MyException("Exception in work()");
}
}
public class MyException extends Exception {
private static final long serialVersionUID = 1L;
public MyException() {
super();
}
public MyException(String message) {
super(message);
}
}
使用JD-GUI的Decompile工具进行反编译main后如下:
public class TestResources
{
public static void main(String[] paramArrayOfString)
throws MyException
{
AutoClose localAutoClose = new AutoClose();Object localObject1 = null;
try
{
localAutoClose.work();
}
catch (Throwable localThrowable2)
{
localObject1 = localThrowable2;throw localThrowable2;
}
finally
{
if (localAutoClose != null) {
if (localObject1 != null) {
try
{
localAutoClose.close();
}
catch (Throwable localThrowable3)
{
((Throwable)localObject1).addSuppressed(localThrowable3);
}
} else {
localAutoClose.close();
}
}
}
}
}
编译器处理了可能为 null 的资源引用,在 finally 块中添加了额外的 if 语句来检查给定资源是否为 null,从而避免对 null 引用调用 close() 时的空指针异常。我们的手动实现中并未这样做,因为资源不可能为 null。但编译器会系统性地生成此类代码。
由于异常屏蔽在实际中是如此重要的一个问题,因此 Java SE 7 扩展了异常,这样就可以将“被抑制的”异常附加到主异常上。我们之前所说的“屏蔽的”异常实际上就是一个被抑制并附加到主异常的异常。
建议:不要声明任何不应被抑制的异常,java.lang.InterruptedException 就是最好的例子。实际上,抑制该异常并将其附加到另一个异常可能会导致忽略线程中断事件,使应用程序处于不一致的状态。
try-with-resources 语句中的资源嵌套问题
上面模拟引用了一个资源,若有多个资源被引用,则try-with-resources语法将在编译的时候为每个资源分别用try包裹并实现类似上述的做法。
如:
private static void compress(String input, String output) throws IOException {
try(
FileInputStream fin = new FileInputStream(input);
FileOutputStream fout = new FileOutputStream(output);
GZIPOutputStream out = new GZIPOutputStream(fout)
) {
byte[] buffer = new byte[4096];
int nread = 0;
while ((nread = fin.read(buffer)) != -1) {
out.write(buffer, 0, nread);
}
}
}
一般来说,一个 try 块声明的资源越多,所生成的代码也就越复杂。因此,可以生成更精简的异常处理块:
private static void compress(String input, String output) throws IOException {
try(
FileInputStream fin = new FileInputStream(input);
GZIPOutputStream out = new GZIPOutputStream(new FileOutputStream(output))
) {
byte[] buffer = new byte[4096];
int nread = 0;
while ((nread = fin.read(buffer)) != -1) {
out.write(buffer, 0, nread);
}
}
}
强调:
好的做法是在 try-with-resources 语句中为每一个持有关键系统资源(如文件描述符、套接字或者 JDBC 连接)的每个资源进行单独声明,必须确保 close() 方法最终得到调用。否则,如果相关资源 API 允许,选择链接分配就不仅是一种惯例:在防止资源泄漏的同时还能得到更为紧凑的代码。
结论
本文介绍了 Java SE 7 中一种新的用于安全管理资源的语言结构。这种扩展带来的影响不仅仅是更多的语法糖。事实上,它能位开发人员生成了正确的代码,消除了编写容易出错的样板代码的需要。更重要的是,这种变化还伴随着将一个异常附加到另一个异常的改进,从而为众所周知的异常彼此屏蔽问题提供了完善的解决方案。
评论区