与面试官聊try-catch-finally关闭资源,你的答案还是10年前的?

简介: 与面试官聊try-catch-finally关闭资源,你的答案还是10年前的?

有编程经验的朋友都知道,在程序运行中如果打开了一些资源,那么当发生异常或程序结束时都需要进行资源的关闭,不然会造成内存溢出的问题。


曾经,关于try-catch-finally的使用也是面试题中的一个热点问题。随着JDK7的发布,情况好像有些变化了,处理资源关闭的方式更加方便了。但如果你的使用方式依旧停留在十年前,那这篇文章中讲到的知识点值得你一读。最重要的是底层原理分析部分。


try-catch-finally传统处理模式

在JDK7之前,我们对异常和资源关闭的处理,通常是通过下面的形式来实现的:


@Test
public void testOldProcess() {
  Scanner scanner = null;
  try {
    scanner = new Scanner(new File("test.txt"));
    while (scanner.hasNext()) {
      System.out.println(scanner.nextLine());
    }
  } catch (FileNotFoundException e) {
    e.printStackTrace();
  } finally {
    if (scanner != null) {
      scanner.close();
    }
  }
}

首先,通过try-catch来捕获异常,并在catch代码块中对异常进行处理(比如打印日志等);


其次,在finally代码块中对打开的资源进行关闭。因为无论程序是否发生异常,finally代码块是必然会被执行的,这也就保证了资源的关闭。


当你写了多年的代码,上面的写法也已经牢记于心,但如果用JDK7及以上版本,且IDE中安装了一些代码规范的插件,在try上面会有如下提示:


'try' can use automatic resource management

1

提示告诉你,try中的代码可以使用自动资源管理了。那我们就来看看它是如何实现自动管理的呢。


JDK7的资源关闭方式

JDK7中引入了一个新特性:“try-with-resource”。先将上面的代码改造成新的实现方式:


@Test
public void testNewProcess() {
  try (Scanner scanner = new Scanner(new File("test.txt"))) {
    while (scanner.hasNext()) {
      System.out.println(scanner.nextLine());
    }
  } catch (FileNotFoundException e) {
    e.printStackTrace();
  }
}

在try后面添加一个小括号,在小括号内声明初始化操作的资源。此时,我们再也不用写finally代码块进行资源的关闭了,JVM会替我们进行资源管理,自动关闭资源。

如果需要声明多个资源,则可以通过分号进行分割:

@Test
public void testNewProcess1() {
  try (
      Scanner scanner = new Scanner(new File("test.txt"));
      Scanner scanner1 = new Scanner(new File("test1.txt"));) {
    while (scanner.hasNext()) {
      System.out.println(scanner.nextLine());
    }
    while (scanner1.hasNext()) {
      System.out.println(scanner1.nextLine());
    }
  } catch (FileNotFoundException e) {
    e.printStackTrace();
  }
}

那么是不是,所有的资源都可以被JVM自动关闭呢?还真不是的,对应的资源类要实现java.io.Closeable接口才行。比如上面的Scanner便是实现了此接口:


public final class Scanner implements Iterator<String>, Closeable {//...}

1

自定义关闭实现

既然实现java.io.Closeable接口的类可以享受自动关闭资源的好处,那我们自定义类是否同样享受这个福利呢?


先定义一个MyResource类,实现java.io.Closeable接口:


public class MyResource implements Closeable {
  public void hello(){
    System.out.println("Hello try-catch-resource");
  }
  @Override
  public void close() throws IOException {
    System.out.println("自定义的close方法被自动调用了...");
  }
}

在自定义类中要实现close()方法。然后看一下使用时是否会被自动关闭:

@Test
public void testMyResource() {
  try (MyResource resource = new MyResource();) {
    resource.hello();
  } catch (IOException exception) {
    exception.printStackTrace();
  }
}

执行单元测试,输入结果:


Hello try-catch-resource

自定义的close方法被自动调用了...

1

2

可以看到在调用hello方法之后,JVM自动调用了close方法,完美的关闭了资源。


底层实现

了解我写文章风格的读者都会知道,在写一个知识点时我们不只会停留在表面,还要看一下它的底层实现。这里我们先将测试代码简化:


public void testMyResource() {
  try (MyResource resource = new MyResource()) {
    resource.hello();
  } catch (IOException e) {
    e.printStackTrace();
  }
}

然后对其class文件进行反编译,可以看到Java编译器对这一些写法的真正实现:

public void testMyResource() {
    try {
        MyResource resource = new MyResource();
        Throwable var2 = null;
        try {
            resource.hello();
        } catch (Throwable var12) {
            var2 = var12;
            throw var12;
        } finally {
            if (resource != null) {
                if (var2 != null) {
                    try {
                        resource.close();
                    } catch (Throwable var11) {
                        var2.addSuppressed(var11);
                    }
                } else {
                    resource.close();
                }
            }
        }
    } catch (IOException var14) {
        var14.printStackTrace();
    }
}

会发现虽然我们没写finally代码块进行资源的关闭,但Java编译器已经帮我们做了处理。看到这里,你可能已经意识到了,try-catch-resource这种写法只是一个语法糖。


但好像不仅仅如此,finally代码中还包含了一个addSuppressed方法的调用,这又是怎么回事呢?下面来分析一下。


避免异常覆盖

在上面的示例中,我们将MyResource的两个方法进行改造:


public class MyResource implements Closeable {
  public void hello(){
    throw new RuntimeException("Resource throw Exception...");
  }
  @Override
  public void close() {
    throw new RuntimeException("Close method throw Exception...");
  }
}

在两个方法中都抛出异常,此时,我们再来执行一下传统写法的单元测试代码:

@Test
public void testOldMyResource() {
  MyResource resource = null;
  try {
    resource = new MyResource();
    resource.hello();
  } finally {
    if (resource != null) {
      resource.close();
    }
  }
}

打印结果如下:


java.lang.RuntimeException: Close method throw Exception...


at com.secbro2.resource.MyResource.close(MyResource.java:19)

at com.secbro2.resource.CloseMyResourcesTest.testOldMyResource(CloseMyResourcesTest.java:22)

   //...


你发现什么了?本来是hello方法先抛出了异常,然后执行close方法又抛出了异常,但后面的异常信息将前面真正的异常信息给“隐藏”了。此时你去排查bug,是不是很困惑?最关键的异常信息被覆盖了。


那么,我们再来执行一下try-catch-resource写法的代码:


@Test

public void testMyResource() {

try (MyResource resource = new MyResource()) {

 resource.hello();

}

}


执行结果如下:


java.lang.RuntimeException: Resource throw Exception...
  at com.secbro2.resource.MyResource.hello(MyResource.java:14)
  at com.secbro2.resource.CloseMyResourcesTest.testMyResource(CloseMyResourcesTest.java:30)
  at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
  at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
  at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
  at java.lang.reflect.Method.invoke(Method.java:498)
  at org.junit.runners.model.FrameworkMethod$1.runReflectiveCall(FrameworkMethod.java:50)
  at org.junit.internal.runners.model.ReflectiveCallable.run(ReflectiveCallable.java:12)
  at org.junit.runners.model.FrameworkMethod.invokeExplosively(FrameworkMethod.java:47)
  at org.junit.internal.runners.statements.InvokeMethod.evaluate(InvokeMethod.java:17)
  at org.junit.runners.ParentRunner.runLeaf(ParentRunner.java:325)
  at org.junit.runners.BlockJUnit4ClassRunner.runChild(BlockJUnit4ClassRunner.java:78)
  at org.junit.runners.BlockJUnit4ClassRunner.runChild(BlockJUnit4ClassRunner.java:57)
  at org.junit.runners.ParentRunner$3.run(ParentRunner.java:290)
  at org.junit.runners.ParentRunner$1.schedule(ParentRunner.java:71)
  at org.junit.runners.ParentRunner.runChildren(ParentRunner.java:288)
  at org.junit.runners.ParentRunner.access$000(ParentRunner.java:58)
  at org.junit.runners.ParentRunner$2.evaluate(ParentRunner.java:268)
  at org.junit.runners.ParentRunner.run(ParentRunner.java:363)
  at org.junit.runner.JUnitCore.run(JUnitCore.java:137)
  at com.intellij.junit4.JUnit4IdeaTestRunner.startRunnerWithArgs(JUnit4IdeaTestRunner.java:68)
  at com.intellij.rt.junit.IdeaTestRunner$Repeater.startRunnerWithArgs(IdeaTestRunner.java:33)
  at com.intellij.rt.junit.JUnitStarter.prepareStreamsAndStart(JUnitStarter.java:230)
  at com.intellij.rt.junit.JUnitStarter.main(JUnitStarter.java:58)
  Suppressed: java.lang.RuntimeException: Close method throw Exception...
    at com.secbro2.resource.MyResource.close(MyResource.java:19)
    at com.secbro2.resource.CloseMyResourcesTest.testMyResource(CloseMyResourcesTest.java:31)
    ... 22 more

此时hello方法中的异常信息和close方法中的异常信息全被打印出来了。而异常信息中多出的Suppressed提示便是通过Java编译器自动添加的addSuppressed方法的调用来实现的。此时,再通过异常日志排查bug是不是简单多了,编译器是真为程序员着想啊。


小结

本文通过对try-catch-finally和try-with-resource两种写法的对比,得知try-with-resource是JDK7为我们提供的一个语法糖,可以让我们的代码更加简洁,本质上与try-catch-finally的效果一样。同时,try-with-resource写法通过addSuppressed方法对异常覆盖问题进行了处理,更便于程序员排查bug。


面试系列

《面试题:聊聊TCP的粘包、拆包以及解决方案》

《面试题:重写equals方法为什么通常会重写hashcode方法?》

《面试官:如何找出字符串中无重复最长子串?》

《还不懂Java的泛型?只用这一篇文章,保证你面试对答如流》

《面试题:将字符串反转的8种方法,你能想到几种?》



目录
相关文章
|
5月前
|
前端开发
CocosCreator 面试题(九)什么是异步加载资源
CocosCreator 面试题(九)什么是异步加载资源
175 0
|
3月前
|
存储 安全 Java
Java面试题:假设你正在开发一个Java后端服务,该服务需要处理高并发的用户请求,并且对内存使用效率有严格的要求,在多线程环境下,如何确保共享资源的线程安全?
Java面试题:假设你正在开发一个Java后端服务,该服务需要处理高并发的用户请求,并且对内存使用效率有严格的要求,在多线程环境下,如何确保共享资源的线程安全?
64 0
|
存储 消息中间件 分布式计算
大数据集群资源预估规划【适用于面试与工作集群规划】
大数据集群资源预估规划【适用于面试与工作集群规划】
601 0
大数据集群资源预估规划【适用于面试与工作集群规划】
|
SQL 缓存 负载均衡
面试官:为什么数据库连接很消耗资源?我竟然答不上来。。一下懵了
面试官:为什么数据库连接很消耗资源?我竟然答不上来。。一下懵了
120 0
面试官:为什么数据库连接很消耗资源?我竟然答不上来。。一下懵了
|
SQL 缓存 负载均衡
面试官:为什么数据库连接很消耗资源,资源都消耗在哪里?
面试官:为什么数据库连接很消耗资源,资源都消耗在哪里?
面试官:为什么数据库连接很消耗资源,资源都消耗在哪里?
【多线程】面试官:如何利用线程工具,防止多线程同时操作一个资源?
通过前面的学习,知道了线程的利与弊,正确的使用多线程,会尽最大的可能去压榨我们系统的资源,从而提高效率,但是如果不合理使用线程,可能会造成副作用,给系统带来更大的压力,进一步的思考,如何才能防止多线程操作一个资源?
|
监控 测试技术 Windows
软件测试面试题:LoadRunner中如何监控Windows资源?
软件测试面试题:LoadRunner中如何监控Windows资源?
95 0
|
前端开发 测试技术
软件测试面试题:在JMeter中是否有必要显式调用嵌入式资源?
软件测试面试题:在JMeter中是否有必要显式调用嵌入式资源?
88 0
|
测试技术 Windows
软件测试面试题:说明如何减少JMeter中的资源需求?
软件测试面试题:说明如何减少JMeter中的资源需求?
96 0
|
资源调度 前端开发 JavaScript
2020 - 2021 年 Web 前端最新导航 - 前端学习资源分享&前端面试资源汇总
2020 - 2021 年 Web 前端最新导航 - 前端学习资源分享&前端面试资源汇总
162 0