java 异常

图片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

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 158,233评论 4 360
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,013评论 1 291
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,030评论 0 241
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,827评论 0 204
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,221评论 3 286
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,542评论 1 216
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,814评论 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,513评论 0 198
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,225评论 1 241
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,497评论 2 244
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 31,998评论 1 258
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,342评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 32,986评论 3 235
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,055评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,812评论 0 194
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,560评论 2 271
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,461评论 2 266

推荐阅读更多精彩内容