Java新特性-try-with-resource

来源:互联网 发布:单片机信号线太长 编辑:程序博客网 时间:2024/04/28 23:28

在我们的项目中,所有被打开的系统资源,比如流、文件或者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();        }    }
原创粉丝点击