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

在Java的江湖中,存在着一种名为finally的功夫,它可以保证当你习武走火入魔之时,还可以做一些自救的操作。在远古时代,处理资源关闭的代码通常写在finally块中。然而,如果你同时打开了多个资源,那么将会出现噩梦般的场景:

  1. public class Demo {
  2. public static void main(String[] args) {
  3. BufferedInputStream bin = null;
  4. BufferedOutputStream bout = null;
  5. try {
  6. bin = new BufferedInputStream(new FileInputStream(new File("test.txt")));
  7. bout = new BufferedOutputStream(new FileOutputStream(new File("out.txt")));
  8. int b;
  9. while ((b = bin.read()) != -1) {
  10. bout.write(b);
  11. }
  12. }
  13. catch (IOException e) {
  14. e.printStackTrace();
  15. }
  16. finally {
  17. if (bin != null) {
  18. try {
  19. bin.close();
  20. }
  21. catch (IOException e) {
  22. throw e;
  23. }
  24. finally {
  25. if (bout != null) {
  26. try {
  27. bout.close();
  28. }
  29. catch (IOException e) {
  30. throw e;
  31. }
  32. }
  33. }
  34. }
  35. }
  36. }
  37. }

Oh My God!!!关闭资源的代码竟然比业务代码还要多!!!这是因为,我们不仅需要关闭BufferedInputStream,还需要保证如果关闭BufferedInputStream时出现了异常, BufferedOutputStream也要能被正确地关闭。所以我们不得不借助finally中嵌套finally大法。可以想到,打开的资源越多,finally中嵌套的将会越深!!!

Java 1.7中新增的try-with-resource语法糖来打开资源,而无需码农们自己书写资源来关闭代码。再也不用担心我把手写断掉了!我们用try-with-resource来改写刚才的例子:

  1. public class TryWithResource {
  2. public static void main(String[] args) {
  3. try (BufferedInputStream bin = new BufferedInputStream(new FileInputStream(new File("test.txt")));
  4. BufferedOutputStream bout = new BufferedOutputStream(new FileOutputStream(new File("out.txt")))) {
  5. int b;
  6. while ((b = bin.read()) != -1) {
  7. bout.write(b);
  8. }
  9. }
  10. catch (IOException e) {
  11. e.printStackTrace();
  12. }
  13. }
  14. }

动手实践

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

  1. public class Connection implements AutoCloseable {
  2. public void sendData() {
  3. System.out.println("正在发送数据");
  4. }
  5. @Override
  6. public void close() throws Exception {
  7. System.out.println("正在关闭连接");
  8. }
  9. }

调用类:

  1. public class TryWithResource {
  2. public static void main(String[] args) {
  3. try (Connection conn = new Connection()) {
  4. conn.sendData();
  5. }
  6. catch (Exception e) {
  7. e.printStackTrace();
  8. }
  9. }
  10. }

运行后输出结果:

  1. 正在发送数据
  2. 正在关闭连接

原理

那么这个是怎么做到的呢?我相信聪明的你们一定已经猜到了,其实,这一切都是编译器大神搞的鬼。我们反编译刚才例子的class文件:

  1. package com.codersm.trywithresource;
  2. public class TryWithResource {
  3. public TryWithResource() {
  4. }
  5. public static void main(String[] args) {
  6. try {
  7. Connection conn = new Connection();
  8. Throwable var2 = null;
  9. try {
  10. conn.sendData();
  11. } catch (Throwable var12) {
  12. var2 = var12;
  13. throw var12;
  14. } finally {
  15. if (conn != null) {
  16. if (var2 != null) {
  17. try {
  18. conn.close();
  19. } catch (Throwable var11) {
  20. var2.addSuppressed(var11);
  21. }
  22. } else {
  23. conn.close();
  24. }
  25. }
  26. }
  27. } catch (Exception var14) {
  28. var14.printStackTrace();
  29. }
  30. }
  31. }

看到没,在第15~27行,编译器自动帮我们生成了finally块,并且在里面调用了资源的close方法,所以例子中的close方法会在运行的时候被执行。

异常屏蔽

细心的你们肯定又发现了,刚才反编译的代码(第21行)比远古时代写的代码多了一个addSuppressed。为了了解这段代码的用意,我们稍微修改一下刚才的例子:我们将刚才的代码改回远古时代手动关闭异常的方式,并且在sendData和close方法中抛出异常:

  1. public class Connection implements AutoCloseable {
  2. public void sendData() throws Exception {
  3. throw new Exception("send data");
  4. }
  5. @Override
  6. public void close() throws Exception {
  7. throw new MyException("close");
  8. }
  9. }

修改main方法:

  1. public class TryWithResource {
  2. public static void main(String[] args) {
  3. try {
  4. test();
  5. }
  6. catch (Exception e) {
  7. e.printStackTrace();
  8. }
  9. }
  10. private static void test() throws Exception {
  11. Connection conn = null;
  12. try {
  13. conn = new Connection();
  14. conn.sendData();
  15. }
  16. finally {
  17. if (conn != null) {
  18. conn.close();
  19. }
  20. }
  21. }
  22. }

运行之后我们发现:

  1. basic.exception.MyException: close
  2. at basic.exception.Connection.close(Connection.java:10)
  3. at basic.exception.TryWithResource.test(TryWithResource.java:82)
  4. at basic.exception.TryWithResource.main(TryWithResource.java:7)
  5. ......

好的,问题来了,由于我们一次只能抛出一个异常,所以在最上层看到的是最后一个抛出的异常——也就是close方法抛出的MyException,而sendData抛出的Exception被忽略了。这就是所谓的异常屏蔽。由于异常信息的丢失,异常屏蔽可能会导致某些bug变得极其难以发现,程序员们不得不加班加点地找bug,如此毒瘤,怎能不除!幸好,为了解决这个问题,从Java 1.7开始,大佬们为Throwable类新增了addSuppressed方法,支持将一个异常附加到另一个异常身上,从而避免异常屏蔽。那么被屏蔽的异常信息会通过怎样的格式输出呢?我们再运行一遍刚才用try-with-resource包裹的main方法:

  1. java.lang.Exception: send data
  2. at basic.exception.Connection.sendData(Connection.java:5)
  3. at basic.exception.TryWithResource.main(TryWithResource.java:14)
  4. ......
  5. Suppressed: basic.exception.MyException: close
  6. at basic.exception.Connection.close(Connection.java:10)
  7. at basic.exception.TryWithResource.main(TryWithResource.java:15)
  8. ... 5 more

可以看到,异常信息中多了一个Suppressed的提示,告诉我们这个异常其实由两个异常组成,MyException是被Suppressed的异常。可喜可贺!

注意事项

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

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

  1. public class TryWithResource {
  2. public static void main(String[] args) {
  3. try (FileInputStream fin = new FileInputStream(new File("input.txt"));
  4. GZIPOutputStream out = new GZIPOutputStream(new FileOutputStream(new File("out.txt")))) {
  5. byte[] buffer = new byte[4096];
  6. int read;
  7. while ((read = fin.read(buffer)) != -1) {
  8. out.write(buffer, 0, read);
  9. }
  10. }
  11. catch (IOException e) {
  12. e.printStackTrace();
  13. }
  14. }
  15. }

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

  1. public void close() throws IOException {
  2. if (!closed) {
  3. finish();
  4. if (usesDefaultDeflater)
  5. def.end();
  6. out.close();
  7. closed = true;
  8. }
  9. }

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

  1. public class TryWithResource {
  2. public static void main(String[] args) {
  3. try (FileInputStream fin = new FileInputStream(new File("input.txt"));
  4. FileOutputStream fout = new FileOutputStream(new File("out.txt"));
  5. GZIPOutputStream out = new GZIPOutputStream(fout)) {
  6. byte[] buffer = new byte[4096];
  7. int read;
  8. while ((read = fin.read(buffer)) != -1) {
  9. out.write(buffer, 0, read);
  10. }
  11. }
  12. catch (IOException e) {
  13. e.printStackTrace();
  14. }
  15. }
  16. }

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

Java基础try-with-resource语法源码分析的更多相关文章

  1. java基础进阶一:String源码和String常量池

    作者:NiceCui 本文谢绝转载,如需转载需征得作者本人同意,谢谢. 本文链接:http://www.cnblogs.com/NiceCui/p/8046564.html 邮箱:moyi@moyib ...

  2. Java ThreadPoolExecutor线程池原理及源码分析

    一.源码分析(基于JDK1.6) ThreadExecutorPool是使用最多的线程池组件,了解它的原始资料最好是从从设计者(Doug Lea)的口中知道它的来龙去脉.在Jdk1.6中,Thread ...

  3. Java入门系列之集合Hashtable源码分析(十一)

    前言 上一节我们实现了散列算法并对冲突解决我们使用了开放地址法和链地址法两种方式,本节我们来详细分析源码,看看源码中对于冲突是使用的哪一种方式以及对比我们所实现的,有哪些可以进行改造的地方. Hash ...

  4. java并发锁ReentrantReadWriteLock读写锁源码分析

    1.ReentrantReadWriterLock 基础 所谓读写锁,是对访问资源共享锁和排斥锁,一般的重入性语义为如果对资源加了写锁,其他线程无法再获得写锁与读锁,但是持有写锁的线程,可以对资源加读 ...

  5. lesson2:java阻塞队列的demo及源码分析

    本文向大家展示了java阻塞队列的使用场景.源码分析及特定场景下的使用方式.java的阻塞队列是jdk1.5之后在并发包中提供的一组队列,主要的使用场景是在需要使用生产者消费者模式时,用户不必再通过多 ...

  6. Java入门系列之集合HashMap源码分析(十四)

    前言 我们知道在Java 8中对于HashMap引入了红黑树从而提高操作性能,由于在上一节我们已经通过图解方式分析了红黑树原理,所以在接下来我们将更多精力投入到解析原理而不是算法本身,HashMap在 ...

  7. Java入门系列之集合LinkedList源码分析(九)

    前言 上一节我们手写实现了单链表和双链表,本节我们来看看源码是如何实现的并且对比手动实现有哪些可优化的地方. LinkedList源码分析 通过上一节我们对双链表原理的讲解,同时我们对照如下图也可知道 ...

  8. Java入门系列之集合ArrayList源码分析(七)

    前言 上一节我们通过排队类实现了类似ArrayList基本功能,当然还有很多欠缺考虑,只是为了我们学习集合而准备来着,本节我们来看看ArrayList源码中对于常用操作方法是如何进行的,请往下看. A ...

  9. Java集合框架之接口Collection源码分析

    本文我们主要学习Java集合框架的根接口Collection,通过本文我们可以进一步了解Collection的属性及提供的方法.在介绍Collection接口之前我们不得不先学习一下Iterable, ...

  10. 《Java Spring框架》Spring IOC 源码分析

    1.下载源码 源码部署:https://www.cnblogs.com/jssj/p/11631881.html 并不强求,最好是有源码(方便理解和查问题). 2. 创建子项目 Spring项目中创建 ...

随机推荐

  1. Tomcat服务器下载、安装、配置环境变量教程(超详细)

    请先配置安装好Java的环境,若没有安装,请参照我以下的步骤进行安装! 请先配置安装好Java的环境,若没有安装,请参照我以下的步骤进行安装! 请先配置安装好Java的环境,若没有安装,请参照我以下上 ...

  2. Shell命令-文件及内容处理之more、less

    文件及内容处理 - more.less 1. more:分页显示文件内容 more命令的功能说明 more 命令类似 cat,不过会以一页一页的形式显示,更方便使用者逐页阅读,而最基本的指令就是按空白 ...

  3. Sublime怎么安装Package control组件

    Sublime怎么安装Package control组件 藏色散人 藏色散人 2018-11-26 14:30:51 原创 Sorry, your browser does not support e ...

  4. JS快速排序 希尔排序 归并排序 选择排序

    /* 快速排序 1.1 算法描述 快速排序由于排序效率在同为O(N*logN)的几种排序方法中效率较高,因此经常被采用,再加上快速排序思想----分治法也确实实用.快速排序是一种既不浪费空间又可以快一 ...

  5. Luogu5283 十二省联考2019异或粽子(trie/可持久化trie+堆)

    做前缀异或和,用堆维护一个五元组(x,l,r,p,v),x为区间右端点的值,l~r为区间左端点的范围,p为x在l~r中最大异或和的位置,v为该最大异或和,每次从堆中取出v最大的元素,以p为界将其切成两 ...

  6. 初步了解Bootstrap4

    Bootstrap 是全球最受欢迎的前端组件库,用于开发响应式布局.移动设备优先的 WEB 项目. Bootstrap4 目前是 Bootstrap 的最新版本,是一套用于 HTML.CSS 和 JS ...

  7. AWS设置允许root登陆

    Refer to the following to set root login: sudo -s (to become root) vi /root/.ssh/authorized_keys Del ...

  8. python 高阶函数之 map

    以例子来理解 用法1:如函数 f(x) = x * x,用python实现如下 >>> def f(x): ... return x * x >>> r = map ...

  9. 「HGOI#2019.4.19省选模拟赛」赛后总结

    t1-Painting 这道题目比较简单,但是我比较弱就只是写了一个链表合并和区间DP. 别人的贪心吊打我的DP,嘤嘤嘤. #include <bits/stdc++.h> #define ...

  10. 洛谷 P1411 树

    最近在做些树形DP练练手 原题链接 大意就是给你一棵树,你可以断开任意数量的边,使得剩下的联通块大小乘积最大. 样例 8 1 2 1 3 2 4 2 5 3 6 3 7 6 8 输出 18 我首先想的 ...