Java基础之try-with-resource语法糖

背景

这是一篇东拼西凑的文章。

在java开发中,一些网络链接或者是文件资源都需要程序员去手动调用close方法关闭,比如InputStream、OutputStream和java.sql.Connection。

如果忘关了就可能造成严重的性能后果。而关闭的方法有很多种。比如finalizer、try-catch-finally、try-with-resources等等。

try-with-resources

try-with-resources是jdk1.7引入的语法糖,使得关闭资源操作无需层层嵌套在finally。

finalizer 会引发内存泄漏

垃圾回收器发现对象实现了finalize()方法并把它们添加到java.lang.ref.Finalizer.ReferenceQueue队列中。

然后Finalizer线程 会把ReferenceQueue里面的对象逐个弹出。

不过由于它的优先级比主线程较低,获取到的CPU时间较少,因此它永远也赶不上主线程创建对象的步伐。

所以会引发内存泄漏,所以finalize()并不适合用作普通的清理工作,一般只用于当java中调用非java代码时重写finalize()方法,并在里面调用本地方法的free()等函数。

或者在一下时候有某些用处:

存在一系列对象,对象中有一个状态为false,如果我们已经处理过这个对象,状态会变为true,为了避免有被遗漏而没有处理的对象,就可以使用finalize()方法:


class MyObject{
 
    boolean state = false;
 
    public void deal(){
        //...一些处理操作
        state = true;
    }
 
    @Override
    protected void finalize(){
        if(!state){
            System.out.println("ERROR:" + "对象未处理!");
        }
    }
    //...

finally的执行顺序

1、finally不是必要条件
也就是说try-catch-finally中,可以只有try-catch,也可以只有try-finally。

2、假设基于try-catch-finally:
第一:代码没有异常
执行顺序:try执行完整->catch不执行->finally执行

第二:代码有异常且catch进行捕获**
执行顺序:try执行部分->跳转catch捕获处理->finally执行

第三:代码有异常且catch不捕获:这种情况没有catch**
执行顺序:try执行部分->finally执行
从上面的执行顺序可以看出,finally语句不管在哪种情况是一定会执行的。基于这个认识,现在我们再来分析。

题外话: 当finally有return时,会直接返回。不会再去返回try或者catch中的返回值,而finally没有return时,try和catch 的return语句并不会马上执行,而是执行完finally代码块之后再返回try和catch里面的值。

try-finally的缺点

同时打开了多个资源,那么将会出现噩梦般的场景:

public class Demo {
		public static void main(String[] args) {
			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")));
				int b;
				while ((b = bin.read()) != -1) {
					bout.write(b);
				}
			}
			catch (IOException e) {
				e.printStackTrace();
			}
			finally {
				if (bin != null) {
					try {
						bin.close();
					}
					catch (IOException e) {
						throw e;
					}
					finally {
						if (bout != null) {
							try {
								bout.close();
							}
							catch (IOException e) {
								throw e;
							}
						}
					}
				}
			}
		}
	}

我们不仅需要关闭 BufferedInputStream ,还需要保证如果关闭 BufferedInputStream 时出现了异常, BufferedOutputStream 也要能被正确地关闭。所以我们不得不借助finally中嵌套finally大法。可以想到,打开的资源越多,finally中嵌套的将会越深。

主要是这样子写代码是真的丑。

用try-with-resource来改写刚才的例子:

	要使用try-with-resource的资源,必须先实现AutoCloseable接口,其中包含了单个返回void的close方法,Java类库与第三方类库中的许多类和接口,现在都实现或扩展了AutoCloseable接口,因此我们现在不必实现了。
public class TryWithResource {
  public static void main(String[] args) {
    try (BufferedInputStream bin = new BufferedInputStream(new FileInputStream(new File("test.txt")));
       BufferedOutputStream bout = new BufferedOutputStream(new FileOutputStream(new File("out.txt")))) {
      int b;
      while ((b = bin.read()) != -1) {
        bout.write(b);
      }
    }
    catch (IOException e) {
      e.printStackTrace();
    }
  }
}

原理

编译器会自动帮我们补全close(),而且可以避免异常屏蔽
看一下反编译出来的代码:

package com.codersm.trywithresource;

	public class TryWithResource {
		public TryWithResource() {
		}

		public static void main(String[] args) {
			try {
				Connection conn = new Connection();
				Throwable var2 = null;

				try {
					conn.sendData();
				} catch (Throwable var12) {
					var2 = var12;
					throw var12;
				} finally {
					if (conn != null) {
						if (var2 != null) {
							try {
								conn.close();
							} catch (Throwable var11) {
								var2.addSuppressed(var11);
							}
						} else {
							conn.close();
						}
					}

				}
			} catch (Exception var14) {
				var14.printStackTrace();
			}

		}
	}

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

异常屏蔽

我们将刚才的代码改回远古时代手动关闭异常的方式,并且在 sendData 和 close 方法中抛出异常:

public class Connection implements AutoCloseable {
		public void sendData() throws Exception {
			throw new Exception("send data");
		}
		@Override
		public void close() throws Exception {
			throw new MyException("close");
		}
	}

修改main方法:

public class TryWithResource {
  public static void main(String[] args) {
    try {
      test();
    }
    catch (Exception e) {
      e.printStackTrace();
    }
  }
  private static void test() throws Exception {
    Connection conn = null;
    try {
      conn = new Connection();
      conn.sendData();
    }
    finally {
      if (conn != null) {
        conn.close();
      }
    }
  }
}

运行之后我们发现:

basic.exception.MyException: close
 at basic.exception.Connection.close(Connection.java:10)
 at basic.exception.TryWithResource.test(TryWithResource.java:82)
 at basic.exception.TryWithResource.main(TryWithResource.java:7)
 ......

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

java.lang.Exception: send data

 at basic.exception.Connection.sendData(Connection.java:5)
 at basic.exception.TryWithResource.main(TryWithResource.java:14)
 ......
 Suppressed: basic.exception.MyException: close
 at basic.exception.Connection.close(Connection.java:10)
 at basic.exception.TryWithResource.main(TryWithResource.java:15)
 ... 5 more

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

仔细对比发现,是因为反编译的代码(第21行)多了一个 addSuppressed :var2.addSuppressed(var11);
从而避免了异常屏蔽的问题。

结论

处理必须关闭的资源时,始终要优先考虑使用try-with-resources,而不是try-finally。这样得到的代码将更简洁,清晰,产生的异常也更有价值,这些也是try-finally无法做到的。

参考文章:
JAVA 内存泄露详解(原因、例子及解决)
为什么推荐使用try-with-resources代替try-finally
深入理解Java基础之try-with-resource语法糖

你可能感兴趣的:(java)