源的初始化与关闭是非常常见的操作,也是很容易出错的地方。Java里一般使用try-catch-finally来处理这个问题,在JDK7增加了try-with-resource。
1.1. try-catch-finally
下面是个有错误的举例:
public void copy_error(File src, File dst) throws IOException { FileInputStream fin = new FileInputStream(src); FileOutputStream fout = new FileOutputStream(dst); try { // do copy } catch (Exception e) { e.printStackTrace(); } finally { fin.close(); fout.close(); } }
上面的代码存在的问题有:
A、 fin、 fout 的初始化没有放入 try - catch 语句,如果 fin 初始化成功,而 fout 初始化时抛出异常,则会导致 fin 不会关闭,因为此时还没有进入 try 语句块,所以 finally 块是不会执行的。
B、 在finally 中的 fin 、 fout 的关闭错误。如果 fin.close() 执行时抛出异常,则不会执行 fout.close 。正确的做法是,由于 Input/OutputStream 实现了 Closeable 接口,可以同等对待,借助可变参数的特点,可以实现这样一个工具方法:( IoUtils.close )
public static void close(Closeable... closeables) { for (Closeable closeable : closeables) { if (closeable != null) { try { closeable.close(); } catch (Exception ignore) { } } } }
关闭流时只需调用IoUtils.close(fin, fout); ,就算只处理单一的输入输出流,这个方法也可用,因为它的参数是可变的。正确的代码大致如下:
public void copy_better(File src, File dst) throws FileNotFoundException { FileInputStream fin = null; FileOutputStream fout = null; try { fin = new FileInputStream(src); fout = new FileOutputStream(dst); // do copy } catch (Exception e) { e.printStackTrace(); } finally { IoUtils.close(fin, fout); } }
涉及SQL 操作也可以先实现一个工具方法:
public static void close(ResultSet rs, Statement stmt, Connection conn) { if (rs != null) { try { rs.close(); } catch (Exception e) { } } if (stmt != null) { try { stmt.close(); } catch (Exception e) { } } if (conn != null) { try { conn.close(); } catch (Exception e) { } } }
代码模板大致是这样:
public void doSql() { Connection conn = null; Statement stmt = null; ResultSet rs = null; try { // conn = ...; // stmt = ...; // rs = stmt.executeQuery(""); // do sql operation } finally { Utils.close(rs, stmt, conn); } }
所以在传统的try-catch-finally 执行资源有关的操作的模板是:
1) 、在try 语句块 前 声明变量;
2) 、在try 语句块 内 初始化变量;
3) 、在finally 语句块中执行关闭操作,且必须确保每个资源的关闭操作得到执行。
1.2. JDK7 try-with-resources
JDK7中提供了 AutoCloseable 接口和 try-with-resource( 也称为 ARM (Automatic Resource Management)
)来减少资源泄露的情况。 下面以SQL 操作为例:
try (Connection conn = DriverManager.getConnection("url"); Statement stmt = conn.createStatement()) { // do sql operation } catch (SQLException e) { e.printStackTrace(); }
我们不需要对在try() 里声明的实现了 AutoCloseable 的资源类进行显式的关闭,新的特性可以确保try() 的小括号里 声明 的资源得到关闭。
但有可能误用而导致资源泄露,下面是个示例:
try (BufferedReader br = new BufferedReader( new FileReader(path))) { br.readLine(); }
问题出在: new BufferedReader( new FileReader(path)) ,如果 FileReader 构建成功,但在构建 BufferedReader 出错,则 br是 null ,不会执行关闭操作;而构建出来的 FileReader 只是 BufferedReader 构造函数的一个参数,ARM 不会对它执行关闭,从而导致资源泄露。避免这种 bug 的做法可以这样:
try (FileReader reader = new FileReader(path)) { BufferedReader br = new BufferedReader(reader); br.readLine(); }
其实这个陷阱在传统的 try-catch-finally 里也存在,只是在 ARM 里更容易误解,因为 ARM 号称自动管理资源。
这个 陷阱 在使用装饰者模式的资源有关的类库里应该容易出现。 JDK 的 io 库在设计时使用了装饰者模式,这样可以动态地扩展功能,比如 InputStream ,基础的字节输入流,被 BufferedInputStream 包装后,又具有了缓冲的功能,再被 DataInputStream 包装后,具有了读取 Java基本数据类型的能力 。 DataInputStream 、 BufferedInputStream 称为装饰者,它们都实现了被装饰者 InputStream的接口。因为它们在类型上一致性,所以可能这样构建一个资源:
new DataInputStream( new BufferedInputStream ( new FileInputStream(path)));
从而导致上面的 陷阱 。
对任何使用装饰者模式实现的与资源有关的类库都应该留心这个 陷阱 。
关于这个 陷阱 还可以查看这篇博客:
http://java.dzone.com/articles/common-try-resources-idiom