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

是否须要看这篇文章?

以下的样例中,假设正常运行返回值多少? 假设出现了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异常。被统一叫做unchecked 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高级特性与最佳实践》第二版 第六章类文件结构 周志明著

原文地址:https://www.cnblogs.com/mfmdaoyou/p/7289520.html