java 异常

作者: n油炸小朋友 | 来源:发表于2018-05-25 00:24 被阅读52次
    图片1

    Throwable 类是 Java 语言中所有错误或异常的超类(这就是一切皆可抛的东西)。它有两个子类:Error和Exception。

    Error:用于指示合理的应用程序不应该试图捕获的严重问题。这种情况是你不能处理的。比如说内存溢出、动态链接异常、虚拟机错误。应用程序不应该抛出这种类型的对象。假如出现这种错误,除了尽力使程序安全退出外,在其他方面是无能为力的。所以在进行程序设计时,应该更关注Exception类。

    Exception:它指出了合理的应用程序想要捕获的条件。Exception又分为两类:一种是CheckedException,一种是UncheckedException。这两种Exception的区别主要是CheckedException需要用try...catch...显示的捕获,而UncheckedException不需要捕获。通常UncheckedException又叫做RuntimeException。

    我们常见的RuntimeExcepiton有IllegalArgumentException、IllegalStateException、NullPointerException、IndexOutOfBoundsException等等。

    对于那些CheckedException就不胜枚举了,我们在编写程序过程中try...catch...捕捉的异常都是CheckedException。io包中的IOException及其子类,这些都是CheckedException。其中 IOException 及其子类异常又被称作 受查异常(编译器在编译期间要求必须得到处理的那些异常)。

    自定义异常类型

    自定义异常类型也是相当简单的,你可以选择继承 Throwable,Exception 或它们的子类,甚至你不需要实现和重写父类的任何方法即可完成一个异常类型的定义。

    如:

    public class MyException extends RuntimeException{
    
    }
    public class MyException extends Exception{
    
    }
    

    如果想要为你的异常提供更多的信息,也可以重写多个重载构造器,例如:

    public class MyException extends RuntimeException{
        public MyException(){}
    
        public MyException(String mess){
            super(mess);
        }
    
        public MyException(String mess,Throwable cause){
            super(mess,cause);
        }
    }
    
    

    我们知道,任意的一个异常类型,无论是 Java API 中的,或是我们自定义的,它们必然会直接或间接继承 Throwable 类。
    而这个 Throwable 类定义了一个 String 类型的 detailMessage 字段存储的由子类传入有关子类异常的详细信息。例如:

    public static void main(String[] args) {
        throw new MyException("hello wrold failed");
    }
    

    输出结果:

    Exception in thread "main" test.exception.MyException: hello wrold failed
        at test.exception.Test.main(Test.java:7)
    

    一个异常出现到最终得到处理的过程

    每当程序遇到一个异常后,Java 会像创建其他对象一样创建一个异常类型的对象,并存储在堆中,接着异常机制接管程序,首先检索当前方法的异常表是否能匹配到该异常(异常表中保存了当前方法已经处理的所有异常集合)。

    如果匹配到一个异常表中的异常,那么将根据异常表中保存的异常处理的相关信息,跳转到处理该异常的字节码位置继续执行。

    否则,虚拟机将终止当前方法的调用并弹栈弹出该方法的栈帧,返回该方法的调用处,继续检索调用者的异常表能够匹配到该异常的处理。

    如果一直无法匹配,最终整个方法调用链中涉及到的所有方法都会弹栈,不会得到正常运行,并且最后虚拟机将打印这个异常的错误信息。

    这就是大致的一个异常出现到最终得到处理的一个过程,足以见得,如果一个异常得到了处理,那么程序将得到恢复并能够继续执行,否则的话所有涉及该异常的方法都将被终止运行。

    异常信息的内容

    至于这个异常信息的内容,我们看看 printStackTrace 方法的具体实现:

    image.png image.png

    总共有三个部分的信息,第一部分由异常的名称及其 detailMessage 构成,第二部分是异常的调用链信息,由上往下的是异常的发生位置到外层方法的调用点,第三部分则是引起该异常的源异常。

    异常的处理方式

    关于异常的处理方式,想必大家最熟悉的就是 try-catch 了吧,try-catch 的基本语法格式如下:

    try{
        //你的程序
    }catch(xxxException e){
        //异常处理代码
    }catch(xxxException e){
        //异常处理代码
    }
    
    

    try 代码块中代码我们又称作「监控区域」,catch 代码块我们称作「异常处理区域」。其中,每一个 catch 代码块对应于一种异常处理,该异常将被保存在方法的异常表中,一旦 try 代码块中产生任何的异常,异常处理机制都会先从异常表检索是否有处理该异常的代码块。

    准确来说,异常表保存的已处理异常块只能用于处理我们 try 块中的代码,别处的相同异常不会被匹配处理。

    当然,除此之外,我们处理异常还有一种方式,抛出异常。例如:

    public static void main(String[] args){
        try{
            calculate(22,0);
        }catch (Exception e){
            System.out.println("捕获一个异常");
            e.printStackTrace();
        }
    }
    
    public static void calculate(int x,int y){
        if (y == 0) 
            throw new MyException("除数为 0");
        int z = x/y;
    }
    

    输出结果:

    捕获一个异常
    test.exception.MyException: 除数为 0
        at test.exception.Test_throw.calculate(Test_throw.java:14)
        at test.exception.Test_throw.main(Test_throw.java:6)
    

    我们可以使用 throw 关键字手动抛出一个异常,这种情况往往是被调用者无力处理某个异常,需要抛给调用者自己处理。
    显然,这种抛出异常的方式算细致的了,并且需要程序员有一定的预判,Java 里还有另一种抛出异常的方式,看:

    public static void calculate2(int x,int y) throws ArithmeticException{
        int z = x/y;
    }
    

    这种方式比较「粗暴」,我不管你什么位置会出现异常,只要你遇到 ArithmeticException 类型的异常,你就给我抛出去。

    其实第二种本质上和第一种也是一样的,虚拟机在进行 x/y 的时候,当发现 y 等于零,也会 new 一个 ArithmeticException 的对象,然后程序交给异常机制。
    但是后者却比前者省事,不用关心你哪个位置会出现异常,也不需要手动做判断,一切都交给虚拟机好了。但是显然的不足点就是有关异常的控制权不在自己手上,某些自定义的异常虚拟机在运行的时候无法判断。

    就比如,假如我们这里的 calculate2 方法不允许 y 等于 1,如果等于 1 就要抛一个 MyException 异常。这种情况,后者怎么也无法实现,因为除数为 1 在虚拟机看来根本不存在任何问题,你叫它如何抛出一个异常。而用前者手动抛一个异常是再简单不过的事情了。

    但是,你必须明确一点的是,无论是使用 throw 手动向上抛出一个异常,还是使用 throws 让虚拟机为我们动态抛出一个异常,你总是需要在某个位置处理这个异常的,这一点需要明确。

    try-catch-finally 的执行顺序

    try-catch-finally 执行顺序的相关问题可以说是各种面试中的「常客」了,尤其是 finally 块中带有 return 语句的情况。我们直接看几道面试题:
    面试题一:

    public static void main(String[] args){
        int result = test1();
        System.out.println(result);
    }
    
    public static int test1(){
        int i = 1;
        try{
            i++;
            System.out.println("try block, i = "+i);
        }catch(Exception e){
            i--;
            System.out.println("catch block i = "+i);
        }finally{
            i = 10;
            System.out.println("finally block i = "+i);
        }
        return i;
    }
    

    输出结果如下:

    try block, i = 2
    finally block i = 10
    10
    

    这算一个相当简单的问题了,没有坑,下面我们稍微改动一下:

    public static int test2(){
        int i = 1;
        try{
            i++;
            throw new Exception();
        }catch(Exception e){
            i--;
            System.out.println("catch block i = "+i);
        }finally{
            i = 10;
            System.out.println("finally block i = "+i);
        }
        return i;
    }
    

    输出结果如下:

    catch block i = 1
    finally block i = 10
    10
    

    运行结果想必也是意料之中吧,程序抛出一个异常,然后被本方法的 catch 块捕获并进行了处理。

    面试题二:

    public static void main(String[] args){
        int result = test3();
        System.out.println(result);
    }
    
    public static int test3(){
        //try 语句块中有 return 语句时的整体执行顺序
        int i = 1;
        try{
            i++;
            System.out.println("try block, i = "+i);
            return i;
        }catch(Exception e){
            i ++;
            System.out.println("catch block i = "+i);
            return i;
        }finally{
            i = 10;
            System.out.println("finally block i = "+i);
        }
    }
    

    输出结果如下:

    try block, i = 2
    finally block i = 10
    2
    

    一旦捕捉到1个异常, 就不会尝试捕捉其他异常.
    finally 代码块中的内容始终会被执行(有两种情况除外, 一是断电, 二是exit函数),无论程序是否出现异常的原因就是,编译器会将 finally 块中的代码复制两份并分别添加在 try 和 catch 的后面。可以看出, finally里的语句, 无论如何都会被执行.

    为什么最后程序依然返回的数值 2 呢?
    在 return 语句返回之前,虚拟机会将待返回的值压入操作数栈,等待返回,即使 finally 语句块对 i 进行了修改,但是待返回的值已经确实的存在于操作数栈中了,所以不会影响程序返回结果。

    面试题三:

    public static int test4(){
        //finally 语句块中有 return 语句
        int i = 1;
        try{
            i++;
            System.out.println("try block, i = "+i);
            return i;
        }catch(Exception e){
            i++;
            System.out.println("catch block i = "+i);
            return i;
        }finally{
            i++;
            System.out.println("finally block i = "+i);
            return i;
        }
    }
    

    运行结果:

    try block, i = 2
    finally block i = 3
    3
    
    

    你会发现程序最终会采用 finally 代码块中的 return 语句进行返回,而直接忽略 try 语句块中的 return 指令。
    最后,对于异常的使用有一个不成文的约定:尽量在某个集中的位置进行统一处理,不要到处的使用 try-catch,否则会使得代码结构混乱不堪。

    throw和throws一些主要区别.

    • throw 写在函数体内, throws写在函数定义语句中.
    • throw 是抛出1个异常对象, throws是有能抛出异常的种类
      所以throw后面的一般加上new 和exception名字().
      而throws后面不能加上new的

    • 一个方法最多只能throw1个异常, 但是可以throws多个种类异常
      因为一旦一个函数throw出1个异常, 这个函数就会被中断执行, 后面的代码被放弃, 如果你尝试在函数内写两个throw, 编译失败.
      而throws 是告诉别人这个函数有可能抛出这几种异常的一种. 但是最多只会抛出一种

    • 如果在一个函数体内throw 1个非runtimeException, 那么必须在函数定义上加上throws后缀. 但反过来就不是必须的.

    参考:
    https://juejin.im/post/5ae1bc78f265da0ba4697f43
    https://www.cnblogs.com/taiwan/p/7073743.html

    相关文章

      网友评论

      本文标题:java 异常

      本文链接:https://www.haomeiwen.com/subject/xipsjftx.html