02_oo_10_try_catch_finally

第二十三章 try语句

1 简介

try语句(try statement)主要用于执行一些可能触发异常(Exception)或者错误(Error)的代码。当使用try...catch语句时,能帮助开发人员捕捉发生的异常或者错误。当使用try-with-resource语句时,能帮助开发人员更好的保护所使用的资源。所以,本章除了介绍try语句的用法以外,还会讲解Java编译器是如何处理try语句的。

2 try...catch...finally语句

try语句有两种形式。第一种形式是try...catch...finally。其中,try包含所需执行的代码块;catch子句是可选的,用于检测与匹配执行过程中发生的异常;finally部分也是可选的,无论在执行过程中是否发生异常,如果发生了异常,无论catch部分是否成功捕获异常,finally部分的代码都被会执行。这是由Java编译器保证的。虽然catch子句与finally子句都是可选的,但是这种形式的try语句必须至少包含一个catch子句或者finally子句。catch子句和finally子句不能同时省略。

如下面的例子所示,Java虚拟机会先执行try子句的代码块,打开/tmp/example.txt文件,并获取当前有多少个字符已被加载至缓冲区中。如果在此过程中,程序发生IOException异常,则会被catch子句捕抓,并执行catch子句的代码,答应错误信息。最后,Java虚拟机执行finally子句,关闭已打开的文件流。

import java.io.FileInputStream;
import java.io.IOException;

public class TryExample {
    public static void main(String[] args) {
        FileInputStream in = null;
        try {
            in = new FileInputStream("/tmp/example.txt");
            int available = in.available(); // 获取多少字符已被加载至缓冲区中
        } catch (IOException ex) {
            // 如果抓捕到了IOException异常
            System.out.println(ex.getMessage());
        } finally {
            try {
                if (in != null)
                    in.close();
            } catch (IOException ex) {
                System.out.println("Can't close the file stream.");
            }
            
        }
    }
}

Java虚拟机在依次执行try部分的代码时,如果发生异常,Java虚拟机会忽略try部分的后续代码,直接跳到catch部分,检查发生的异常是否与catch子句中捕抓的异常匹配。在try语句中,开发人员可以使用多个catch子句(Catch Clause)捕抓多个不同的异常。或者,开发人员也可以使用一个catch子句,捕抓多个不同的异常。异常匹配是指发生的异常类型与捕抓异常的类型相同,或者是其子类。Java虚拟机会依次检查catch子句中给出的异常类型,直到发现第一个匹配的异常为止,并进入相对应的异常处理代码块。如果在catch子句中未发现匹配的异常,则可认为该try语句抛出了这个未捕获的异常,从而跳到上一级try语句的catch子句处继续查找匹配的异常,直至找到为止,或者程序异常退出。

try语句的执行结果可按照以下几类场景确定,我们用表来描述这些场景。在表中,除了场景序号以外,其他四栏分别表示try子句的执行结果,catch子句的执行结果,finally子句的执行结果,和整个try语句的执行结果。例如,第一个场景可以理解为:当try子句执行成功时,如果catch子句被省略,并且finally子句也执行成功,那么,整个try语句可被认为执行成功。以此类推其他场景。

场景try子句执行结果catch子句执行结果finally子句执行结果try语句执行结果
1执行成功省略执行成功执行成功
2执行成功省略执行失败,抛出异常,但未能成功捕获异常执行失败,抛出未捕获的异常
3执行成功省略执行失败,抛出异常,但成功捕获该异常,则可认为finally子句执行成功执行成功
4执行失败,抛出了异常未能捕获该异常省略执行失败,抛出未捕获的异常
5执行失败,抛出了异常未能捕获该异常执行成功执行失败,抛出未捕获的异常
6执行失败,抛出了异常成功捕获异常,并执行成功省略执行成功
7执行失败,抛出了异常成功捕获异常,但在执行catch子句的过程中抛出了另一个异常省略执行失败,抛出未捕获的异常
8执行失败,抛出了异常成功捕获异常。在执行catch子句的过程又抛出了另一个异常,但成功捕获的这个异常省略执行成功
9执行失败,抛出了异常省略执行成功执行失败,抛出未捕获的异常
10执行失败,抛出了异常省略执行失败,抛出了另一个异常执行失败,抛出finally子句中的异常。try子句中的异常会被丢弃。
11执行失败,抛出了异常成功捕获异常。但在执行过程中,又抛出了另一个异常省略或者执行成功执行失败,抛出catch子句中的未捕获的异常
12执行失败,抛出了异常成功捕获异常。但在执行过程中,又抛出了另一个异常执行失败,抛出了第三个异常执行失败,抛出finally子句中的异常。catch子句中的异常会被丢弃。

下面是两个try...catch...finally语句的例子。第一个例子在一个catch子句中尝试捕抓IndexOutOfBoundsException和NumberFormatException异常。第二个例子在两个catch子句中分别捕抓这两个异常。在第二个例子中,异常对象可以都命名为ex,它们并不会相互冲突,因为它们不在同一个作用域内。

public class TryExample {
    public static void main(String[] args) {
        // Example 1
        try{
            String str = "100";
            String strOfLastDigit = str.substring(2);
            Integer i = Integer.valueOf(strOfLastDigit);
            System.out.println("Last digit is " + i);
        } catch (IndexOutOfBoundsException | NumberFormatException ex) {
            // Exception Handler
            System.out.println(ex.getMessage());
        } 
        
        // Example 2
        try{
            String str = "100";
            String strOfLastDigit = str.substring(2);
            Integer i = Integer.valueOf(strOfLastDigit);
            System.out.println("Last digit is " + i);
        } catch (IndexOutOfBoundsException ex) {
            // ClassNotFoundException Handler
            System.out.println(ex.getMessage());
        } catch (NumberFormatException ex) {
            // ClassNotFoundException Handler
            System.out.println(ex.getMessage());
        } 
    }
}

3 try-with-resource语句

3.1 try-with-resource基本用法

try-with-resource语句有两种形式。第一种形式的try-with-resource语句可以在括号中初始化一个资源变量。变量初始化的过程会在try代码块运行前执行。在try代码块执行完毕后,资源变量所表示的资源会自动释放。资源释放是通过java.lang.AutoCloseable接口实现的。资源变量的类型需要实现AutoCloseable接口,Java虚拟机会自动调用这个接口释放资源。第二种形式的try-with-resource语句可直接使用一个已初始化的资源变量。在try代码块运行完毕后,Java虚拟机会自动释放资源。第二种形式的try-with-resource语句在Java 9中引入。

值得注意的是,这里的资源变量必须是final变量或者可被认为是final的变量(effectively final,这种变量是可以被声明为final,但是开发人员没有将其声明为final的变量)。如果开发人员使用了第二种形式的try-with-resource语句,Java编译器会定义一个临时变量,并将其转换为第一种形式的try-with-resource语句。

当资源变量初始化时抛出了异常,则try语句的代码块不会被执行。整个try语句会被认为执行失败,抛出这个异常。try语句无需释放资源。当资源变量初始化成功,但是在执行try代码块的时候失败,抛出异常,则停止执行剩余的try代码块中的代码,资源被自动释放,整个try语句抛出这个异常。

如下面的代码所示,readFile2成员方法使用的是第二种形式的try-with-resource语句。实际上,它会被Java编译器转化为readFile3成员方法中所示的代码。

import java.io.IOException;
import java.io.FileInputStream;

public class TryExample {
    public void readFile1(String filename) throws IOException {
        try (FileInputStream input = new FileInputStream(filename)) {
            int avail = input.available();
        }
        // input is closed.
    }

    public void readFile2(String filename) throws IOException {
        FileInputStream input = new FileInputStream(filename);
        try (input) { 
            int avail = input.available();
        }
        // input is closed.
    }
    
    public void readFile3(String filename) throws IOException {
        FileInputStream input = new FileInputStream(filename);
        try (FileInputStream tempInput = input) {
            int avail = input.available();
        }
        // input/tempInput is closed.
    }
}

3.2 多资源try-with-resource用法

更复杂的情况是,try-with-resource语句可接受多个资源变量。在这种情况下,Java编译器会按照顺序将这个包含多个资源的try-with-resource语句变成多个包含单个资源的try-with-resource语句,并将每个语句依次嵌入在另一个语句里。如下面的例子所示,Java编译器会将readFile1函数内的try-with-resource语句转化为readFile2函数中的语句。因此,try语句的嵌套层次也确定了多个资源初始化的顺序和释放的顺序。

import java.io.IOException;
import java.io.FileInputStream;

public class TryExample {
    public void readFile1(String filename1, String filename2) throws IOException {
        try (FileInputStream input1 = new FileInputStream(filename1);
             FileInputStream input2 = new FileInputStream(filename2)
        ) {
            int aval1 = input1.available();
            int aval2 = input2.available();
        }
        // input1 and input2 are closed.
    }

    public void readFile2(String filename1, String filename2) throws IOException {
        try (FileInputStream input1 = new FileInputStream(filename1)) {
            try (FileInputStream input2 = new FileInputStream(filename2)) {
                int aval1 = input1.available();
                int aval2 = input2.available();
            }
            // input 2 is closed
        }
        // input1 is closed.
    }
}

3.3 try-with-resource语句的扩展形式

try-with-resource语句也可使用catch子句和finally子句,其使用方法和try...catch...finally语句类似。当遇到try-with-resource语句的扩展形式时,Java编译器会使用try-with-resource的基本形式(见本章3.1)将资源保护起来,然后将其放置在try...catch...finally语句中。如下面的例子所示,Java编译器会将readFile1成员方法中的语句转换成readFile2成员方法中的语句。

import java.io.IOException;
import java.io.FileInputStream;

public class TryExample {
    public void readFile1(String filename) throws IOException {
        try (FileInputStream input = new FileInputStream(filename)) {
            int avail = input.available();
        } catch (IOException ex) {
            System.out.println("An IOException occurs.");
        } finally {
            System.out.println("finally clause is invoked.");
        }
    }

    public void readFile2(String filename) throws IOException {
        try {
            try (FileInputStream input = new FileInputStream(filename)) {
                int avail = input.available();
            }
        } catch (IOException ex) {
            System.out.println("An IOException occurs.");
        } finally {
            System.out.println("finally clause is invoked.");
        }
    }
}

在这种情况下的异常处理与try...catch...finally语句的处理类似。如果在内层的try-with-resource语句执行失败,则可认为是外层try代码块执行失败,Java编译器会在catch子句中寻找匹配的异常处理代码块,并在最后执行finally子句。

唯一的一个例外是Suppressed Exception List。当执行try-with-resource语句时,如果资源变量初始化发生异常Ex1或者try代码块发生异常Ex1,而且在释放资源时也发生了异常Ex2,此时,try-with-resource语句会被认为执行失败,抛出异常Ex1。但是,Ex2不会被丢弃。Ex2会被当做Suppressed Exception加入到Ex1的Suppressed Exception List中。当在处理Ex1异常时,Ex2异常对象可通过Ex1.getSuppressed()获取。类似的,当释放多个资源时,如果发生了两个以上的异常,则后发生的异常会被加入到第一个异常的Suppressed Exception List中。因此,try-with-resource语句会被认为执行失败,抛出第一个发生的异常。

4 结语

本章介绍了try语句的两种形式,以及几种基本的变化和用法。从Java编译器和虚拟机的角度看,这两种try语句的形式是可以相互转换的,其对于异常的处理过程也类似。try-with-resource语句为开发人员提供了语法上的便利,但是,在开发过程中,开发人员需要注意如何处理资源释放时可能产生的异常。

一般来说,这种情况可采用两种策略。第一种策略思想是,如果释放资源时发生异常,说明该资源已处于错误状态。在这种情况下,程序可以忽略该错误(捕抓该错误,但是并不处理该错误)。

另一种策略思想是,既然资源释放时产生错误,程序应该对其进行额外的保护,要么确保以后不会再使用该资源,要么将其暂时保留,等待其恢复后,再释放。这两种策略无优劣之分,可按照应用场景选取。

 

上一章
下一章

注册用户登陆后可留言

Copyright  2019 Little Waterdrop, LLC. All Rights Reserved.