finally
执行流程
- 在try或catch中有
System.exit(0);
这类终止JVM的的语句,那么finally语句不会执行。 - 若try或catch中有return,先执行return后的表达式运算,然后把要返回的值保存下来,设try中要返回的结果为x,则在跳转finally之前会先将此结果存放到不同于x的局部变量(栈)中,再执行finally,待执行完finally后再返回之前保存的值,因此,即使finally中对变量x进行了改变也不会影响返回结果(也就是说,若在finally中直接修改的是引用地址或基本数据类型,那么返回的就是拷贝之前的,若在finally中修改要返回的某个对象中的属性,则返回的是修改过后的)。
- 若finally块中有return则提前退出,此时返回值不是try或catch中return处保存的值,因此finally中的return是一定会被return的,编译器把finally中的return实现为一个warning。
示例
private static int test1(int x) {
try {
return x++;
} finally {
x++;
}
}
private static int test2(int x) {
try {
return x++;
} finally {
return x++;
}
}
private static int test3(int x) {
try {
return ++x;
} finally {
// 虽然此处执行,但对返回结果没影响,因为return处已把返回结果保存在栈中。
++x;
}
}
private static int test4(int x) {
try {
return x++;
} finally {
++x;
}
}
private static int test5(int x) {
try {
return ++x;
} finally {
return ++x;
}
}
private static int test6() {
try {
return 1;
} finally {
return 2;
}
}
public static void main(String[] args) {
System.out.println(test1(0)); // 0
System.out.println(test2(0)); // 1
System.out.println(test3(0)); // 1
System.out.println(test4(0)); // 0
System.out.println(test5(0)); // 2
System.out.println(test6()); // 2
}
try-with-resource
try-finally的缺点
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中的嵌套将会越深。
使用
必须先实现AutoCloseable接口并重写close方法。
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()并避免异常屏蔽。
例:
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");
}
public static void main(String[] args) {
try (Connection conn = new Connection()) {
conn.sendData();
} catch (Exception e) {
e.printStackTrace();
}
}
}
反编译class文件
public static void main(String[] args) {
try {
Connection conn = new Connection();
try {
conn.sendData();
} catch (Throwable var5) {
try {
conn.close();
} catch (Throwable var4) {
var5.addSuppressed(var4);
}
throw var5;
}
conn.close();
} catch (Exception var6) {
var6.printStackTrace();
}
}
发现反编译的代码不仅补全了close,还多了addSuppressed。
异常屏蔽
Throwable类的addSuppressed方法支持将一个异常附加到另一个异常身上,从而避免异常屏蔽。
- 使用addSuppressed:
java.lang.Exception: send data
at xyz.jshen6.common.Connection.sendData(Connection.java:5)
at xyz.jshen6.common.Connection.main(Connection.java:15)
Suppressed: xyz.jshen6.common.MyException: close
at xyz.jshen6.common.Connection.close(Connection.java:10)
at xyz.jshen6.common.Connection.main(Connection.java:14)
异常信息中多了一个Suppressed的提示,说明这个异常其实由两个异常组成,MyException是被Suppressed的异常。
- 未使用addSuppressed:
private static void test() throws Exception {
Connection conn = null;
try {
conn = new Connection();
conn.sendData();
} finally {
if (conn != null) {
conn.close();
}
}
}
public static void main(String[] args) {
try {
test();
} catch (Exception e) {
e.printStackTrace();
}
}
xyz.jshen6.common.MyException: close
at xyz.jshen6.common.Connection.close(Connection.java:10)
at xyz.jshen6.common.Connection.test(Connection.java:20)
at xyz.jshen6.common.Connection.main(Connection.java:27)
由于一次只能抛出一个异常,所以在最上层看到的是最后一个抛出的异常,也就是close方法抛出的MyException,而sendData抛出的Exception被忽略了,这就是异常屏蔽。由于异常屏蔽导致异常信息的丢失,可能会使一些bug变得极难发现。
注意事项
一定要了解资源的close方法内部的实现逻辑再使用try-with-resource,否则还是可能会导致资源泄露。
在Java BIO中采用了大量的装饰器模式,当调用装饰器的close方法时,本质上是调用了装饰器内部包裹的流的close方法,比如:
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中,而out变量代表的是被装饰的FileOutputStream类(GZIPOutputStream是FileOutputStream的装饰器),根据try-with-resource的特性,实际编译后的代码会调用fin.close()
和out.close()
。
再看下ZIPOutputStream的父类DeflaterOutputStream的close源码:
public void close() throws IOException {
if (!closed) {
finish();
if (usesDefaultDeflater)
def.end();
out.close();
closed = true;
}
}
在调用out变量的close方法之前,GZIPOutputStream还做了finish操作,该操作会继续往FileOutputStream中写压缩信息,若此时出现异常,out.close()方法则不执行,然而这才是最底层的关闭资源的方法,所以应该在try-with-resource中单独声明最底层的资源,确保对应的close方法一定能够被调用:
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();
}
}
反编译class:
public static void main(String[] args) {
try {
FileInputStream fin = new FileInputStream(new File("input.txt"));
try {
FileOutputStream fout = new FileOutputStream(new File("out.txt"));
try {
GZIPOutputStream out = new GZIPOutputStream(fout);
try {
byte[] buffer = new byte[4096];
int read;
while((read = fin.read(buffer)) != -1) {
out.write(buffer, 0, read);
}
} catch (Throwable var9) {
try {
out.close();
} catch (Throwable var8) {
var9.addSuppressed(var8);
}
throw var9;
}
out.close();
} catch (Throwable var10) {
try {
fout.close();
} catch (Throwable var7) {
var10.addSuppressed(var7);
}
throw var10;
}
fout.close();
} catch (Throwable var11) {
try {
fin.close();
} catch (Throwable var6) {
var11.addSuppressed(var6);
}
throw var11;
}
fin.close();
} catch (IOException var12) {
var12.printStackTrace();
}
}