在我们的项目中,所有被打开的系统资源,比如流、文件或者Socket连接等,都需要被开发者手动关闭,否则随着程序的不断运行,资源泄露将会累积成重大的生产事故。
语法
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();
}
}
}
}
为了解决以上传统方式的问题, 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()的代码,这样肯定能够保证真正的流被关闭。
总结:同时打开多个资源时,单独声明每个资源!
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();
}
}