Java异常处理机制难点解惑-用代码说话

x33g5p2x  于2021-03-13 发布在 Java  
字(5.6k)|赞(0)|评价(0)|浏览(209)

是否需要看这篇文章?

下面的例子中,如果正常执行返回值多少? 如果出现了ArithmeticException返回值多少? 如果出现非ArithmeticException(如NullPointerException)返回值多少?
如果你了解这个例子说明的问题,并了解例子中三种情况下的执行细节,这篇文章你就不用浪费时间看了。
例子:

    public int testException_finally(){
        int x;
        try {
            x = 1;
//int y = 1/0;  //放开此处,出现ArithmeticException。

/*//注释掉 int y = 1/0;处,放开此处,出现NullPointerException
            String str = null; 
            str.substring(0);
            */
            return x;
        } catch (ArithmeticException e) {
            x =2;
            return x;
        } finally{
            x = 3;
        }
    }

答案:
如果正常执行,返回值为1;
如果抛出ArithmeticException,返回值为2;
如果抛出其他Exception,则抛出该Exception,无返回值。

一看就知晓的同学们,散了吧。这篇文章你们不需要看了。
不知所云的同学们,请继续往下看。下面要说的正适合你☺

1. Java异常的分类

Throwable类是Java语言中所有异常和错误的基类。Throwable有两个直接子类:Error和Exception。Error用来表示编译期或系统错误,一般不用我们程序员关心(现在还是程序猿,但有一颗想做架构师的心☺);Exception是可以被抛出的异常的基本类型,这个才是我们需要关心的基类。

Java异常在设计时的基本理念是用名称代表发生的问题,异常的名称应该可以望文知意。异常并非全是在java.lang包中定义,像IO异常就定义在java.io包中。在Exception中,有一种异常:RuntimeException(运行时异常)不需要我们在程序中专门捕获,Java会自动捕获此种异常,RuntimeException及其子类异常再加上Error异常,被统一叫做unecked Exception(非检查异常);其他的Exception及其子类异常(不包括RuntimeException及其子类异常),被统一叫做checked Exception(检查型异常)。对于检查型异常,才是我们需要捕获并处理的异常。非检查型异常Java会自动捕获并抛出。当然,我们也可以主动捕获RuntimeException型异常。但是Error型异常一般不去捕获处理。

2. Java异常处理的基本规则

对于可能发生异常的Java代码块,我们可以将其放入try{}中,然后在try之后使用catch(***Exception e){}处理抛出的具体异常,如果没有匹配的catch处理抛出的异常,则会将该异常向上一层继续抛出,直到抛至Main()方法。

有一些代码,我们希望不管try中的代码是成功还是失败都需要执行,那么这些代码我们就可以放在finally{}中。

Java的异常处理采用的是终止模型,即如果try块中的某处出现了异常,则立刻停止当前程序的运行,在堆中创建对应的异常对象,异常的处理转入到异常处理代码处(即对应的catch块),执行完异常处理代码后,try块中出现异常处之后的程序将不会被执行,程序会跳出try块,执行try块之外的程序。

例子:覆盖知识点:①执行对应的catch;②一定执行finally中代码;③try出异常之后的代码不再执行;

public String testException_one(){
        String str = "aaa";
        try {
            str += "bbb";
            int a = 1/0;
            str += "ccc";
        } catch (NullPointerException e) {
            str += "ddd";
        } catch (ArithmeticException e) {
            str += "eee";
        } finally {
            str += "fff";
        }
        str += "ggg";
        return str;
    }

程序执行返回结果:aaabbbeeefffggg

注意:没有输出ccc和ddd。

结果分析:上面的程序进入try块后,连接了bbb,然后遇到1/0抛出ArithmeticException 异常,首先NullPointerException所在的catch块不匹配该异常,然后检查到ArithmeticException 所在的catch块匹配该异常,进入该catch块内进行异常处理。执行完所在的catch块,一定会执行finally块,但是try块报异常行之后的代码不会再执行,直接跳出try块,继续执行try…catch…finally之后的代码。

3. 继承和实现接口时的异常限制

class OneException extends Exception{}
class TwoException extends Exception{}
class OneSonException extends OneException{}
class TwoSonException extends TwoException{}

interface Worker {
    void work() throws TwoException;

    void say() throws OneException;
}

class Person {

    public Person() throws TwoException {
        System.out.println("Person Constructor...");
    }

    public void eat() throws OneException {
        System.out.println("Person eat...");
    }

    public void say() throws TwoException {
        System.out.println("Person say...");
    }

}

public class Coder extends Person implements Worker {

    /**
     * 此处的TwoException是必须的,因为Person的构造函数中抛出了TwoException。
     * Coder在调用父类构造函数时,也必须抛出次异常,且不能是其子类异常.另外,构造函数可以抛出比父类多的异常。
     * @throws TwoException
     * @throws OneException
     */
    public Coder() throws TwoException, OneException {
        super();
    }

    /**
     * 实现的接口的方法或者重写的父类的方法可以抛出原方法的异常或其子类异常或者不抛出异常,
     * 但是不能抛出原方法没有声明的异常。这样是为了多态时,当子类向上转型为基类执行方法时,基类的方法依然有效。
     */
    public void work() throws TwoSonException {
        // TODO Auto-generated method stub
    }

    /**
     * 在接口和父类中都有该方法,且异常声明不是同一个异常,则该方法的声明不能抛出任何异常,
     * 因为子类中的该方法在多态时必须同时满足其实现的接口和继承的基类的异常要求。不能抛出比基类或接口方法声明中更多的异常。
     */
    public void say(){

    }

    /**
     * 基类中eat方法抛出了异常,在子类中覆盖该方法时,可以不声明抛出异常
     */
    public void eat(){

    }
}
/**同时还应该注意,如果方法声明抛出的是RunTimeException类型的异常,不受以上的限制;
只有检查型异常才受以上限制。非检查型异常由于系统自动捕获,不受任何限制。
*
*/

4. finally一定会执行

①break/continue/while:如下面例子中所示在循环中遇到continue或break时,finally也会执行。

public void testException_two(){

        for(int i = 0; i < 5; i++){
            try {
                if(i == 0){
                    continue;
                }
                if(i == 1){
                    throw new Exception();
                }
                if(i == 3){
                    break;
                }
                System.out.println("try..." + i);
            } catch (Exception e) {
                System.out.println("catch..." + i);
            } finally {
                System.out.println("finally..." + i);
            }
        }

    }
    /*
    执行结果:
finally...0
catch...1
finally...1
try...2
finally...2
finally...3
         */

②return:即使在try块中正常执行了return,finally也在return之前执行了。如下面例子所示:

    public void testException_three(){
        int a = 1;
        try {
            System.out.println("try...");
            return;
        } catch (Exception e) {
            // TODO: handle exception
        } finally{
            System.out.println("finally...");
        }
    }
    /*
执行结果:
try...
finally...
     */

③还有一种情况是:当try块抛出异常时,如果没有catch块能捕获到该异常,则该异常会被抛至上一级,在被抛至上一级之前,finally块会被执行,然后异常才会被抛至上一级。这个请有兴趣的同学自己验证吧。

总之,finally中的代码是一定会被执行到的。

5. finally中丢失异常

因为finally的特殊性,还会造成异常丢失的情况,如果在finally中抛出异常或者在finally中使用了return,则在try块中抛出的异常将会被系统丢掉。如下面代码所示(OneException和TwoException的定义在上面异常限制一节中已经给出):

    public void testException_finally_one(){
        try {
            System.out.println("test finally...");
            try {
                if(1 == 1){
                    throw new OneException();   
                }
            }finally{
                throw new TwoException();
            }
        } catch (Exception e) {
            System.out.println("e.getClass: " + e.getClass());
        }
    }
    /*
     * 
     执行结果输出:
test finally...
e.getClass: class com.synnex.demo.TwoException
     */

    public void testException_finally_two(){
        try {
            System.out.println("test finally...");
            try {
                if(1 == 1){
                    throw new OneException();   
                }
            }finally{
                return;
            }
        } catch (Exception e) {
            System.out.println("e.getClass: " + e.getClass());
        }
    }

    /*
     执行结果输出:
test finally...
     */

6. finally造成的返回值困惑

下面进入到本篇开始的那个例子的解惑。
例子:

    public int testException_finally(){
        int x;
        try {
            x = 1;
//int y = 1/0;  //放开此处,出现ArithmeticException。

/*//注释掉 int y = 1/0;处,放开此处,出现NullPointerException
            String str = null; 
            str.substring(0);
            */
            return x;
        } catch (ArithmeticException e) {
            x =2;
            return x;
        } finally{
            x = 3;
        }
    }

答案:
如果正常执行,返回值为1;
如果抛出ArithmeticException,返回值为2;
如果抛出其他Exception,则抛出该Exception,无返回值。

解惑:这是我根据《深入理解Java虚拟机-JVM高级特性与最佳实践》第二版书中的例子(P187~P188)做了一些修改。出现这种情况的原因是:在没有出现异常的情况下,先执行了x=1;然后执行return x;时,首先是将x的一个副本保存在了方法栈帧的本地变量表中,执行return之前必须执行finally中的操作:x=3;将x的值设置为了3,但是return时是将本地变量表中保存的x的那个副本拿出来放到栈顶返回。故没出异常时,返回值为1;出ArithmeticException异常或其子类异常时,返回值是2;如果出现非ArithmeticException异常,则执行完x=3之后,将异常抛出至上一层,没有返回值。
对字节码命令熟悉的朋友可以使用javap -verbose等命令反编译出该方法的字节码命令和异常表,从字节码层面上就能清晰的看出执行过程了。我对字节码命令知道得还不够多,只能从大体上解释这种运行过程。以后字节码命令学得自认为可以了的时候,也会写字节码相关的文章出来。希望这篇文章能帮到一些人理解Java的异常处理机制。

参考文章及书籍:
Java异常的深入研究与分析
《Java编程思想》第四版中文版第十二章通过异常处理错误
《深入理解Java虚拟机-JVM高级特性与最佳实践》第二版 第六章类文件结构 周志明著

相关文章