Java新特性-try-with-resource

在我们的项目中,所有被打开的系统资源,比如流、文件或者Socket连接等,都需要被开发者手动关闭,否则随着程序的不断运行,资源泄露将会累积成重大的生产事故。

传统的try

语法

try{
    //进行可能出现异常的操作
}catch(捕获的异常名称){
    //进行异常处理
}finally{
    //关闭资源操作
}

实例代码如下:

public static void try1() {
        BufferedInputStream bin = null;
        BufferedOutputStream bout = null;
        try {
            bin = new BufferedInputStream(new FileInputStream(new File("test.txt")));
            bout = new BufferedOutputStream(new FileOutputStream(new File("out.txt")));
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            if (bin != null) {
                try {
                    bin.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
            if (bout != null) {// 确保即使BufferedInputStream在执行close()方法时发生异常,也执行BufferedOutputStream的close()方法
                try {
                    bout.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
    }
  • 使用finally块来关闭物理资源,保证关闭操作总是会被执行。
  • 关闭每个资源之前首先保证引用该资源的引用变量不为null。
  • 为每一个物理资源使用单独的try…catch块来关闭资源,保证关闭资源时引发的异常不会影响其他资源的关闭。
  • 以上方式导致finally块代码十分臃肿,关闭资源的代码比业务代码还要多,程序的可读性降低。

java7增强try

为了解决以上传统方式的问题, Java7新增了自动关闭资源的try语句。它允许在try关键字后紧跟一对圆括号,里面可以声明、初始化一个或多个资源,此处的资源指的是那些必须在程序结束时显示关闭的资源(数据库连接、网络连接等),try语句会在该语句结束时自动关闭这些资源。

语法

try(初始化可能出现异常的操作){
    //进行操作
}ctach(捕获的异常名称){
    //进行异常操作
}

打开一个资源

try(
    打开一个资源的语句;//末尾可以加";",也可以不加
){
    //进行操作
}

打开多个资源

try(
    打开一个资源的语句;
    打开一个资源的语句;
    打开一个资源的语句;//多个表达式末尾加";",最后一个表达式末尾可以不加";"
){
    //进行操作
}

改写以上代码如下:

    //捕获异常
    public static void try2() {
        try (BufferedInputStream bin = new BufferedInputStream(new FileInputStream(new File("test.txt")));
                BufferedOutputStream bout = new BufferedOutputStream(new FileOutputStream(new File("out.txt")));) {
            // 执行完try中的语句过后,资源自动关闭
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

或者

    //抛出异常
    public static void try2() throws FileNotFoundException, IOException {
        try (BufferedInputStream bin = new BufferedInputStream(new FileInputStream(new File("test.txt")));
                BufferedOutputStream bout = new BufferedOutputStream(new FileOutputStream(new File("out.txt")));) {
            // 执行完try中的语句过后,资源自动关闭
        }
    }

自动关闭资源的try语句相当于包含了隐式的finally块(用于关闭资源),因此这个try语句可以既没有catch块,也没有finally块。

动手测试

为了能够配合try-with-resource,资源必须实现AutoCloseable接口。且该接口的实现类需要重写close方法:

package tryTest;

/**
 * 
 * @ClassName: Connection
 * @Description: 创建实例,实现AutoCloseable接口
 * @author cheng
 * @date 2017年8月18日 上午9:37:11
 */
public class Connection implements AutoCloseable {

    public void sendData() throws Exception {
        System.out.println("正在发送数据........");
    }

    /**
     * 重写close方法
     */
    @Override
    public void close() throws Exception {
        System.out.println("正在自动关闭连接........");
    }

}

测试

    public static void try3() {
        try (Connection conn = new Connection();) {
            conn.sendData();
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

运行结果:

正在发送数据........
正在自动关闭连接........

通过结果我们可以看到,close方法被自动调用了。

异常屏蔽处理

修改Connection,模拟抛出异常

package tryTest;

/**
 * 
 * @ClassName: Connection
 * @Description: 创建实例,实现AutoCloseable接口
 * @author cheng
 * @date 2017年8月18日 上午9:37:11
 */
public class Connection implements AutoCloseable {

    public void sendData() throws Exception {
        System.out.println("正在发送数据........");
        throw new Exception("模拟发送时发生了异常.....");
    }

    /**
     * 重写close方法
     */
    @Override
    public void close() throws Exception {
        System.out.println("正在自动关闭连接........");
        throw new Exception("模拟关闭时发生了异常.....");
    }

}

使用传统的try,手动关闭异常

    public static void try5() throws Exception {
        Connection conn = null;
        try {
            conn = new Connection();
            conn.sendData();
        } finally {
            if (conn != null) {
                conn.close();
            }
        }
    }

调用:

    public static void main(String[] args) {
        try {
            try5();
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

运行结果:

正在发送数据........
正在自动关闭连接........
java.lang.Exception: 模拟关闭时发生了异常.....
    at tryTest.Connection.close(Connection.java:23)
    at tryTest.TryTest.try5(TryTest.java:40)
    at tryTest.TryTest.main(TryTest.java:26)

好的,问题来了,由于我们一次只能抛出一个异常,所以在最上层看到的是最后一个抛出的异常——也就是close方法抛出的模拟异常,而sendData抛出的Exception被忽略了。这就是所谓的异常屏蔽。

由于异常信息的丢失,异常屏蔽可能会导致某些bug变得极其难以发现,程序员们不得不加班加点地找bug.

为了解决这个问题,从Java 1.7开始,大佬们为Throwable类新增了addSuppressed方法,支持将一个异常附加到另一个异常身上,从而避免异常屏蔽。那么被屏蔽的异常信息会通过怎样的格式输出呢?我们再运行一遍刚才用try-with-resource包裹的main方法:

    public static void try3() {
        try (Connection conn = new Connection();) {
            conn.sendData();
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

运行结果:

正在发送数据........
正在自动关闭连接........
java.lang.Exception: 模拟发送时发生了异常.....
    at tryTest.Connection.sendData(Connection.java:14)
    at tryTest.TryTest.try3(TryTest.java:59)
    at tryTest.TryTest.main(TryTest.java:23)
    Suppressed: java.lang.Exception: 模拟关闭时发生了异常.....
        at tryTest.Connection.close(Connection.java:23)
        at tryTest.TryTest.try3(TryTest.java:60)
        ... 1 more

可以看到,异常信息中多了一个Suppressed的提示,告诉我们这个异常其实由两个异常组成.

打开多个资源时:单独声明每个资源

在使用try-with-resource的过程中,一定需要了解资源的close方法内部的实现逻辑。否则还是可能会导致资源泄露。

比如,在Java BIO中采用了大量的装饰器模式。当调用装饰器的close方法时,本质上是调用了装饰器内部包裹的流的close方法。比如:

public class TryWithResource {
    public static void main(String[] args) {
        try (FileInputStream fin = new FileInputStream(new File("input.txt"));
                GZIPOutputStream out = new GZIPOutputStream(new FileOutputStream(new File("out.txt")))) {
            byte[] buffer = new byte[4096];
            int read;
            while ((read = fin.read(buffer)) != -1) {
                out.write(buffer, 0, read);
            }
        }
        catch (IOException e) {
            e.printStackTrace();
        }
    }
}

在上述代码中,我们从FileInputStream中读取字节,并且写入到GZIPOutputStream中。GZIPOutputStream实际上是FileOutputStream的装饰器。由于try-with-resource的特性,实际编译之后的代码会在后面带上finally代码块,并且在里面调用fin.close()方法和out.close()方法。我们再来看GZIPOutputStream类的close方法:

public void close() throws IOException {
    if (!closed) {
        finish();
        if (usesDefaultDeflater)
            def.end();
        out.close();
        closed = true;
    }
}

我们可以看到,out变量实际上代表的是被装饰的FileOutputStream类。在调用out变量的close方法之前,GZIPOutputStream还做了finish操作,该操作还会继续往FileOutputStream中写压缩信息,此时如果出现异常,则会out.close()方法被略过,然而这个才是最底层的资源关闭方法。正确的做法是应该在try-with-resource中单独声明最底层的资源,保证对应的close方法一定能够被调用。在刚才的例子中,我们需要单独声明每个FileInputStream以及FileOutputStream:

public class TryWithResource {
    public static void main(String[] args) {
        try (FileInputStream fin = new FileInputStream(new File("input.txt"));
                FileOutputStream fout = new FileOutputStream(new File("out.txt"));
                GZIPOutputStream out = new GZIPOutputStream(fout)) {
            byte[] buffer = new byte[4096];
            int read;
            while ((read = fin.read(buffer)) != -1) {
                out.write(buffer, 0, read);
            }
        }
        catch (IOException e) {
            e.printStackTrace();
        }
    }
}

由于编译器会自动生成fout.close()的代码,这样肯定能够保证真正的流被关闭。

总结:同时打开多个资源时,单独声明每个资源!

注意

  • 被自动关闭的资源必须实现Closeable或AutoCloseable接口。(Closeable是AutoCloseable的子接口,Closeable接口里的close()方法声明抛出了IOException,;AutoCloseable接口里的close()方法声明抛出了Exception)
  • 被关闭的资源必须放在try语句后的圆括号中声明、初始化。如果程序有需要自动关闭资源的try语句后可以带多个catch块和一个finally块。
  • Java7几乎把所有的“资源类”(包括文件IO的各种类,JDBC编程的Connection、Statement等接口……)进行了改写,改写后的资源类都实现了AutoCloseable或Closeable接口

AutoCloseable 源码:

package java.lang;

public interface AutoCloseable {

    void close() throws Exception;

}

Closeable源码:

package java.lang;

public interface Closeable implements AutoCloseable {

    void close() throws IOException;

}

若我们在try中打开的资源类没有实现AutoCloseable接口或者Closeable接口,则会报以下错误:

The resource type Connection does not implement java.lang.AutoCloseable

捕获多个异常

当我们操作一个对象的时候,它有时候会抛出多个异常,例如:

    public static void try6() {
        try {
            Thread.sleep(20000);
            FileInputStream fis = new FileInputStream("/a/b,txt");
        } catch (InterruptedException e) {
            e.printStackTrace();
        } catch (FileNotFoundException e) {
            e.printStackTrace();
        }
    }

这样写起来很繁琐,java7优化后代码如下:

    public static void try7() {
        try {
            Thread.sleep(20000);
            FileInputStream fis = new FileInputStream("/a/b,txt");
        } catch (InterruptedException | FileNotFoundException e) {
            e.printStackTrace();
        }
    }

并且catch语句后面的异常参数是final的,不可以再修改

处理反射异常

java7之前使用反射时会有许多需要处理的异常,代码如下:

    public static void try7() {
        Object object = null;
        Class<?> clazz;
        try {
            clazz = Class.forName("tryTest.Connection");
            clazz.getMethods()[0].invoke(object);
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        } catch (IllegalAccessException e) {
            e.printStackTrace();
        } catch (IllegalArgumentException e) {
            e.printStackTrace();
        } catch (InvocationTargetException e) {
            e.printStackTrace();
        } catch (SecurityException e) {
            e.printStackTrace();
        }
    }

尽管可以使用上面提到的捕获多个异常技术进行改良,

    public static void try7() {
        Object object = null;
        Class<?> clazz;
        try {
            clazz = Class.forName("tryTest.Connection");
            clazz.getMethods()[0].invoke(object);
        } catch (ClassNotFoundException | IllegalAccessException | IllegalArgumentException | InvocationTargetException
                | SecurityException e) {
            e.printStackTrace();
        }
    }

但还是十分繁琐,java7引入了新的ReflectiveOperationException 异常类,帮助我们捕获反射异常

    public static void try7() {
        Object object = null;
        Class<?> clazz;
        try {
            clazz = Class.forName("tryTest.Connection");
            clazz.getMethods()[0].invoke(object);
        } catch (ReflectiveOperationException e) {
            e.printStackTrace();
        }
    }
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值