4.5异常设计和捕获原理
目录介绍
- 01.异常处理的方式
- 1.0 不捕获异常会怎样
- 1.1 异常处理两种方式
- 1.2 两种方式使用场景
- 1.3 它们的区别
- 1.4 异常处理关键字
- 1.5 如何选合适处理方法
- 1.6 异常处理两种模型
- 1.7 异常先抛出后捕获
- 1.8 异常使用指南
- 02.try-catch处理异常
- 2.1 try-catch处理异常格式
- 2.2 作用说明
- 2.4 try-catch注意事项
- 2.5 没有捕获异常分析
- 2.6 try-catch-finally执行顺序
- 2.7 try和catch基本用法
- 2.8 try-catch-finally规则
- 03.throw和throws抛出异常
- 3.1 throws的用法介绍
- 3.2 throw的用法介绍
- 3.3 throws和throw区别
- 3.4 Throws抛出异常的规则
- 05.捕获异常的建议
- 5.1 捕获原始的异常
- 5.2 不要生吞异常
- 5.3 捕获代码不要太多
- 5.4 不要捕获所有异常
- 5.5 不要用异常处理机制代替判断
- 5.8 不要打印堆栈后再抛出异常
- 06.问题思考答疑
- 6.1 如何捕获异步异常
01.异常处理的方式
1.0 不捕获异常会怎样
- 没有被程序捕获的所有异常,最终都会交由Java运行时系统提供的默认处理程序捕获。
- 默认处理程序会显示一个描述异常的字符串,输出异常发生的堆栈踪迹并终止程序。例如,运行如下代码:
public class Demo { public static void main(String[] args) { int a=520/0; } }
- 默认处理程序会显示一个描述异常的字符串,输出异常发生的堆栈踪迹并终止程序。例如,运行如下代码:
- 生成的异常信息:
Exception in thread "main" java.lang.ArithmeticException: / by zero at Demo.main(Demo.java:5)
- 抛出的异常的类型是Exception的子类ArithmeticException,即算术异常,更具体地描述了发生的错误类型。Java提供了一些能与可能产生的各种运行时错误相匹配的内置异常类型。
1.1 异常处理两种方式
- 异常处理的方式有两种:
- 第一种是:try-catch-finally,捕获异常
- 第二种是:throws,抛出异常给外部开发者
1.2 两种方式使用场景
1.3 它们的区别
1.4 异常处理关键字
- Java异常机制用到的几个关键字:try、catch、finally、throw、throws。
- • try -- 用于监听。将要被监听的代码(可能抛出异常的代码)放在try语句块之内,当try语句块内发生异常时,异常就被抛出。
- • catch -- 用于捕获异常。catch用来捕获try语句块中发生的异常。
- • finally -- finally语句块总是会被执行。它主要用于回收在try块里打开的物理资源(如数据库连接、网络连接和磁盘文件)。只有finally块,执行完成之后,才会回来执行try或者catch块中的return或者throw语句,如果finally中使用了return或者throw等终止方法的语句,则就不会跳回执行,直接停止。
- • throw -- 用于抛出异常。
- • throws -- 用在方法签名中,用于声明该方法可能抛出的异常。
1.5 如何选合适处理方法
- 如何使用异常处理,需要注意那些原则,有哪些区别?
- 原则:如果该功能内部可以将问题处理,用try;如果处理不了,交由调用者处理,这是用throws
- 区别:后续程序需要继续运行就try;后续程序不需要继续运行就throws;如果JDK没有提供对应的异常,需要自定义异常。
1.6 异常处理两种模型
- 异常的处理理论上有两种模型,一种是终止模型,另一种模型是恢复模型。
- 终止模型。这种模型中,会假设错误的出现会让程序无法返回到异常发生的地方继续执行。异常被抛出意味着错误已经无法挽回。这也是 Java 和 C++ 所支持的模型。
- 恢复模型。异常处理程序的工作是修正错误,然后尝试重新调用出问题的方法。在 Java 中可以在 while 循环内放入 try 块,达到类似的效果。这种效果一般会导致耦合度过高--恢复性处理程序的出口一般是非通用型代码(针对特殊异常情况),不好维护。
- 目前终止模型已经基本取代了恢复模型,虽然这种恢复模型看起来很好,但是实际上使用起来并不容易。
1.7 异常先抛出后捕获
- 一个方法所能捕捉的异常,一定是Java代码在某处所抛出的异常。简单地说,异常总是先被抛出,后被捕捉的。
- 处理运行时异常。
- 由于运行时异常的不可查性,为了更合理、更容易地实现应用程序,Java规定,运行时异常将由Java运行时系统自动抛出,允许应用程序忽略运行时异常。
- 处理Error。
- 对于方法运行中可能出现的Error,当运行方法不欲捕捉时,Java允许该方法不做任何抛出声明。因为,大多数Error异常属于永远不能被允许发生的状况,也属于合理的应用程序不该捕捉的异常。
- 处理可查异常。
- 对于所有的可查异常,Java规定:一个方法必须捕捉,或者声明抛出方法之外。也就是说,当一个方法选择不捕捉可查异常时,它必须声明将抛出异常。
1.8 异常使用指南
- 总结起来,应该在以下情况使用异常:
- 在恰当的级别处理问题。(在知道该如何处理的情况下才捕获异常)
- 解决问题并且重新调用产生异常的方法。
- 进行少许修补,然后绕过异常发生的地方继续执行。
- 用别的数据进行计算,以代替方法预计会返回的值。
- 把当前运行环境下能做的事情尽量做完,然后把相同的异常重抛到更高层。
- 把当前运行环境下能做的事情尽量做完,然后把不同的异常抛到更高层。
- 进行简化。(如果异常模式使问题变得太复杂,那么会很难使用)
- 让类库和程序更安全。(这既是在为调试做短期投资,也是为程序的健壮性做长期投资)
02.try-catch处理异常
2.1 try-catch处理异常格式
- try...catch处理异常的基本格式
try { 可能出现问题的代码 ; }catch(异常名 变量名){ 针对问题的处理 ; }finally{ 释放资源; }
2.2 作用说明
- 三个代码块说明
- try 块:用于捕获异常。其后可接零个或多个catch块,如果没有catch块,则必须跟一个finally块。
- catch 块:用于处理try捕获到的异常。
- finally 块:无论是否捕获或处理异常,finally块里的语句都会被执行。有些情况则不会执行,下面会说到。
- 异常处理说明
- 使用上述结构,可以在 try 内部代码抛出异常的时候,捕获到异常,并对异常进行处理。try 内的部分可以称之为监控区域(guared region)。
- 每一个 catch 子句(异常处理程序)看起来就像一个接收且仅接收异常这种特殊类型参数的方法。当异常被抛出时,异常处理机制会负责搜寻参数与异常类型相匹配的第一个处理程序,然后进入子句中执行,此时便认为异常得到了处理。一旦 catch 子句结束,则处理程序的查找过程结束。
2.4 try-catch注意事项
- a:try中的代码越少越好
- b:catch中要做处理,哪怕是一条输出语句也可以.(不能将异常信息隐藏)
- c:处理多个异常
- 1:能明确的尽量明确,不要用大的来处理。
- 2:平级关系的异常谁前谁后无所谓,如果出现了子父关系,父必须在后面。
2.5 没有捕获异常分析
- 程序明明出现了异常,也catch(Exception e)了,却没有捕获到任何信息。
- 原因无非有两个:
- 1.异常所在的线程跟你捕获的线程不是同一个线程;
- 2.程序抛出的不是Exception而是Error。Error跟Exception一样都继承自Throwable,是指不应该被捕获的严重错误。
- 为什么不该捕获Error呢
- 因为出现Error的情况会造成程序直接无法运行,所以捕获了也没有任何意义。
2.6 try-catch-finally执行顺序
- 1)当try没有捕获到异常时:
- try语句块中的语句逐一被执行,程序将跳过catch语句块,执行finally语句块和其后的语句;
- 2)当try捕获到异常,catch语句块里没有处理此异常的情况:
- 当try语句块里的某条语句出现异常时,而没有处理此异常的catch语句块时,此异常将会抛给JVM处理。finally语句块里的语句还是会被执行,但finally语句块后的语句不会被执行;
- 3)当try捕获到异常,catch语句块里有处理此异常的情况:
- 在try语句块中是按照顺序来执行的,当执行到某一条语句出现异常时,程序将跳到catch语句块,并与catch语句块逐一匹配,找到与之对应的处理程序,其他的catch语句块将不会被执行,而try语句块中,出现异常之后的语句也不会被执行,catch语句块执行完后,执行finally语句块里的语句,最后执行finally语句块后的语句;
2.7 try和catch基本用法
- 看一下下面案例代码
public class YC { public static void main(String[] args) { try { int i = 10/0; System.out.println("i="+i); } catch (ArithmeticException e) { System.out.println("Caught Exception"); System.out.println("e.getMessage(): " + e.getMessage()); System.out.println("e.toString(): " + e.toString()); System.out.println("e.printStackTrace():"); e.printStackTrace(); } } }
- 运行结果
Caught Exception e.getMessage(): / by zero e.toString(): java.lang.ArithmeticException: / by zero e.printStackTrace(): java.lang.ArithmeticException: / by zero at YC.main(YC.java:6)
- 结果说明:
- 在try语句块中有除数为0的操作,该操作会抛出java.lang.ArithmeticException异常。
- 通过catch,对该异常进行捕获。观察结果我们发现,并没有执行System.out.println("i="+i)。
- 这说明try语句块发生异常之后,try语句块中的剩余内容就不会再被执行了。
2.8 try-catch-finally规则
- 必须在 try 之后添加 catch 或 finally 块。try 块后可同时接 catch 和 finally 块,但至少有一个块。
- 必须遵循块顺序:若代码同时使用 catch 和 finally 块,则必须将 catch 块放在 try 块之后。
- catch 块与相应的异常类的类型相关。
- 一个 try 块可能有多个 catch 块。若如此,则执行第一个匹配块。即Java虚拟机会把实际抛出的异常对象依次和各个catch代码块声明的异常类型匹配,如果异常对象为某个异常类型或其子类的实例,就执行这个catch代码块,不会再执行其他的 catch代码块
- 可嵌套 try-catch-finally 结构。
- 在 try-catch-finally 结构中,可重新抛出异常。
- 除了下列情况,总将执行 finally 做为结束:JVM 过早终止(调用 System.exit(int));在 finally 块中抛出一个未处理的异常;计算机断电、失火、或遭遇病毒攻击。
03.throw和throws抛出异常
3.1 throws的用法介绍
- throws的具体使用规则。
- 1.用在方法声明后面,跟的是异常类名;2.可以跟多个异常类名,用逗号隔开;3.表示抛出异常,由该方法的调用者来处理;4.throws表示出现异常的一种可能性,并不一定会发生这些异常。
static void pop() throws NegativeArraySizeException { // 定义方法并抛出NegativeArraySizeException异常 int[] arr = new int[-3]; // 创建数组 } public static void main(String[] args) { // 主方法 try { // try语句处理异常信息 pop(); // 调用pop()方法 } catch (NegativeArraySizeException e) { System.out.println("pop()方法抛出的异常");// 输出异常信息 } }
- 1.用在方法声明后面,跟的是异常类名;2.可以跟多个异常类名,用逗号隔开;3.表示抛出异常,由该方法的调用者来处理;4.throws表示出现异常的一种可能性,并不一定会发生这些异常。
- 注意点
- 使用throws关键字将异常抛给调用者后,如果调用者不想处理该异常,可以继续向上抛出,但最终要有能够处理该异常的调用者。
- pop方法没有处理异常NegativeArraySizeException,而是由main函数来处理。
3.2 throw的用法介绍
- throw的一般形式如下所示
- XxxException必须是Throwable或其子类类型的对象。
- throw语句之后的执行流程会立即停止,所有的后续语句都不会执行,然后按顺序依次检查所有的catch语句,检查是否和异常类型相匹配。如果没有找到匹配的catch语句,那么默认的异常处理程序会终止程序并输出堆栈踪迹。
throw new XxxException();
- throw的具体使用方法
- 用在方法体内,跟的是异常对象名
- 只能抛出一个异常对象名,这个异常是自己new出来的
- 表示抛出异常,由方法体内的语句处理
- 例如,运行以下代码,将得到输出结果:“NullPointerException”。下面这个案例可以正常捕获……
public class Demo { public static void demo(){ throw new NullPointerException("NullPointer"); } public static void main(String[] args) { try{ demo(); }catch (NullPointerException e) { System.out.println("NullPointerException"); } } }
- 但如果catch子句的异常与throw抛出的异常类型不匹配时,异常将由Java默认的异常处理程序来处理。
public class Demo { public static void demo(){ throw new NullPointerException("NullPointer"); } public static void main(String[] args) { try{ demo(); }catch (ArrayIndexOutOfBoundsException e) { System.out.println("ArrayIndexOutOfBoundsException"); } } }
- 运行结果是:
Exception in thread "main" java.lang.NullPointerException: NullPointer at Demo.demo(Demo.java:4) at Demo.main(Demo.java:9)
3.3 throws和throw区别
- throws的方式处理异常:
- 定义功能方法时,需要把出现的问题暴露出来让调用者去处理。那么就通过throws在方法上标识。
- throw的概述:
- 在功能方法内部出现某种情况,程序不能继续运行,需要进行跳转时,就用throw把异常对象抛出。
3.4 Throws抛出异常的规则
- 1.如果是不可查异常(unchecked exception),即Error、RuntimeException或它们的子类,那么可以不使用throws关键字来声明要抛出的异常,编译仍能顺利通过,但在运行时会被系统抛出。
- 2.必须声明方法可抛出的任何可查异常(checked exception)。即如果一个方法可能出现受可查异常,要么用try-catch语句捕获,要么用throws子句声明将它抛出,否则会导致编译错误
- 3.仅当抛出了异常,该方法的调用者才必须处理或者重新抛出该异常。当方法的调用者无力处理该异常的时候,应该继续抛出,而不是囫囵吞枣。
- 4.调用方法必须遵循任何可查异常的处理和声明规则。若覆盖一个方法,则不能声明与覆盖方法不同的异常。声明的任何异常必须是被覆盖方法所声明异常的同类或子类。
05.捕获异常的建议
5.1 捕获原始的异常
- 为什么开发中建议开发者捕获异常而不要抛出异常?先开看第一个吧,下面的代码反映了异常处理中哪些不当之处?
- 示例如下。很多人误以为捕获泛化的 Exception 更省事,但也更容易让人“丈二和尚摸不着头脑”。相反,捕获原始的异常能够让协作者更轻松地辨识异常类型,更容易找出问题的根源。
try { // 业务代码 // … Thread.sleep(1000L); } catch (Exception e) { // Ignore it }
- 示例如下。很多人误以为捕获泛化的 Exception 更省事,但也更容易让人“丈二和尚摸不着头脑”。相反,捕获原始的异常能够让协作者更轻松地辨识异常类型,更容易找出问题的根源。
- 这段代码虽然很短,但是已经违反了异常处理的两个基本原则
- 尽量不要捕获类似 Exception 这样的通用异常,而是应该捕获特定异常。
- 在这里是 Thread.sleep()抛出的InterruptedException。这是因为在日常的开发和合作中,我们读代码的机会往往超过写代码,软件工程是门协作的艺术,所以我们有义务让自己的代码能够直观地体现出尽量多的信息,而泛泛的 Exception 之类,恰恰隐藏了我们的目的。
- 另外,我们也要保证程序不会捕获到我们不希望捕获的异常。比如,你可能更希望 RuntimeException 被扩散出来,而不是被捕获。进一步讲,除非深思熟虑了,否则不要捕获 Throwable 或者 Error,这样很难保证我们能够正确程序处理 OutOfMemoryError。
5.2 不要生吞异常
- 这是异常处理中要特别注意的事情,因为很可能会导致非常难以诊断的诡异情况。
- 生吞异常,往往是基于假设这段代码可能不会发生,或者感觉忽略异常是无所谓的,但是千万不要在产品代码做这种假设!
- 如果我们不把异常抛出来,或者也没有输出到日志(Logger)之类,程序可能在后续代码以不可控的方式结束。
- 没人能够轻易判断究竟是哪里抛出了异常,以及是什么原因产生了异常。
5.3 捕获代码不要太多
- 再来看看第二段代码案例
try { // 业务代码1 // 业务代码2 // 业务代码3 } catch (IOException e) { e.printStackTrace(); }
- 这段代码作为一段实验代码,它是没有任何问题的,但是在产品代码中,通常都不允许这样处理。你先思考一下这是为什么呢?
- 我们先来看看printStackTrace()的文档,开头就是“Prints this throwable and its backtrace to the standard error stream”。
- 问题就在这里,在稍微复杂一点的生产系统中,标准出错(STERR)不是个合适的输出选项,代码太多了,因为你很难判断出到底输出到哪里去了。
5.4 不要捕获所有异常
- 可以只写一个异常处理程序来捕获所有类型的异常。
- 通过捕获异常类型的基类 Exception,就可以做到这一点(事实上还有其他的基类,但 Exception是同编程活动相关的基类)
catch(Exception e){ System. out. println(“Caught an exception”); }
- 通过捕获异常类型的基类 Exception,就可以做到这一点(事实上还有其他的基类,但 Exception是同编程活动相关的基类)
- 这将捕获所有异常,所以最好把它放在处理程序列表的末尾,以防它抢在其他处理程序之前先把异常捕获了。因为 Exception是与编程有关的所有异常类的基类,所以它不会含有太多具体的信息,不过可以调用它从其基类 Throwable继承的方法:
String getMessage() String getLocalizedMessage()
- 用来获取详细信息,或用本地语言表示的详细信息。
5.5 不要用异常处理机制代替判断
- 本来应该判 null 的,结果使用了异常处理机制来代替。
public static void main(String[] args) { try { String str = null; String[] strs = str.split(","); } catch (NullPointerException e) { e.printStackTrace(); } }
5.8 不要打印堆栈后再抛出异常
- 不要打印堆栈后再抛出异常
- 当异常发生时打印它,然后重新抛出它,以便调用者能够适当地处理它。就像下面这段代码一样。但是这样做的坏处是调用者可能也打印了异常,重复的打印信息会增添排查问题的难度。
public static void main(String[] args) throws IOException { try { InputStream is = new FileInputStream("yc.txt"); }catch (IOException e) { e.printStackTrace(); throw e; } }
- 当异常发生时打印它,然后重新抛出它,以便调用者能够适当地处理它。就像下面这段代码一样。但是这样做的坏处是调用者可能也打印了异常,重复的打印信息会增添排查问题的难度。
06.问题思考答疑
6.1 如何捕获异步异常
- 可以思考一个问题,对于异常处理编程,不同的编程范式也会影响到异常处理策略
- 比如,现在非常火热的反应式编程(Reactive Stream),因为其本身是异步、基于事件机制的,所以出现异常情况,决不能简单抛出去;
- 另外,由于代码堆栈不再是同步调用那种垂直的结构,这里的异常处理和日志需要更加小心,我们看到的往往是特定 executor 的堆栈,而不是业务方法调用关系。
- 对于这种情况,你有什么好的办法吗?
06.catch执行注意要点
6.1 思考一个问题
- 一旦某个catch捕获到匹配的异常类,其它的catch还会执行吗?
- 一旦某个catch捕获到匹配的异常类型,将进入异常处理代码。一经处理结束,就意味着整个try-catch语句结束。其他的catch子句不再有匹配和捕获异常类型的机会。
- 对于有多个catch子句的异常程序而言,应该尽量将捕获底层异常类的catch子句放在前面,同时尽量将捕获相对高层的异常类的catch子句放在后面。否则,捕获底层异常类的catch子句将可能会被屏蔽。
- 举个例子:RuntimeException异常类包括运行时各种常见的异常,ArithmeticException类和ArrayIndexOutOfBoundsException类都是它的子类。因此,RuntimeException异常类的catch子句应该放在最后面,否则可能会屏蔽其后的特定异常处理或引起编译错误。
6.3 多条catch子句
- 在有些情况下,一个代码块可能会引发多个异常。对于这种情况,需要指定两条或多条catch子句,用于捕获不同类型的异常。当抛出异常时,按顺序检查每条catch语句,执行类型和异常相匹配的第一条catch子句,忽略其他catch子句,并继续执行try/catch代码块后面的代码。
- 需要注意的是,异常子类必须位于异常超类之前,因为使用了某个超类的catch语句会捕获这个超类及其所有子类的异常。因此,如果子类位于超类之后的话,永远也不会到达子类。不可到达的代码会被编译器提示错误。
- 参照如下代码,通过Math的静态方法random来随机产生0和1两个随机数,生成的不同数值就会引发不同的异常,分别由不同的catch子句进行处理
public static void main(String[] args) { int random=(int) Math.round(Math.random()); try{ int a=10/random; int[] array={10}; array[random]=1; }catch (ArithmeticException e) { System.out.println("ArithmeticException"); }catch (ArrayIndexOutOfBoundsException e) { System.out.println("ArrayIndexOutOfBoundsException"); } System.out.println("代码块结束"); }
- 此外,也可以通过多重捕获的方式来使用相同的catch子句捕获两个或更多个异常。在catch子句中使用或运算符(|)分隔每个异常,每个多重捕获参数都被隐式地声明为final类型。
public static void main(String[] args) { int random=(int) Math.round(Math.random()); try{ int a=10/random; int[] array={10}; array[random]=1; }catch(ArithmeticException | ArrayIndexOutOfBoundsException e){ System.out.println("两个异常之一"); } }
6.3 catch异常匹配
- 抛出异常的时候,异常处理系统会按照代码的书写顺序找出“最近”的处理程序。找到匹配的处理程序之后,他就认为异常将得到处理,然后就不再继续查找。
- 查找的时候并不要求抛出的异常和处理程序所声明的异常完全匹配。派生类的对象也可以匹配其基类的处理程序。
- 如果把捕获基类的 catch 子句放在最前面,然后将后面放上派生类异常的 catch 子句,那么编译器会发现无法捕获派生类的异常,然后就会报错。下面刚好用一个案例说明。
6.4 catch中注意异常层级关系
- 捕获异常时,为何在catch中要注意异常层级关系?需要注意哪些问题?
- 注意,catch中一定要注意层级关系。这里举一个简单的案例,就可以很好的理解为何要注重层级问题呢!
public static void start() throws IOException, RuntimeException{ throw new RuntimeException("Not able to Start"); } public static void main(String args[]) { try { start(); } catch (Exception e1) { e1.printStackTrace(); } catch (RuntimeException e2) { e2.printStackTrace(); } }
- 注意,catch中一定要注意层级关系。这里举一个简单的案例,就可以很好的理解为何要注重层级问题呢!
- 这段代码会在捕捉异常代码块的RuntimeException类型变量“e2”里抛出编译异常错误。因为Exception是RuntimeException的超类,在start方法中所有的RuntimeException会被第一个捕捉异常块捕捉,这样就无法到达第二个捕捉块,这就是抛出“exception java.lang.RuntimeException has already been caught”的编译错误原因。
08.处理重新抛出异常
有时希望把刚捕获的异常重新抛出,尤其是在使用Exception捕获所有异常的时候。既然已经得到了对当前异常对象的引用,可以直接把它重新抛出:看下
@UiThread public void onDestroy() { try { ao.e.a(true); String var1; al.b((var1 = this.C.a(com.google.android.libraries.navigation.internal.vo.e.a.f)) == null, var1); this.a(com.google.android.libraries.navigation.internal.zn.i.A_); NavigationApi.a().b(this.A); this.w.clear(); this.e = false; } catch (RuntimeException | Error var3) { com.google.android.libraries.navigation.internal.wk.a.c(var3); throw var3; } }
重抛异常会把异常抛给上一级环境中的异常处理程序,同一个try块的后续catch子句将被忽略。此外,异常对象的所有信息都得以保持,所以高一级环境中捕获此异常的处理程序可以从这个异常对象中得到所有信息。
如果只是把当前异常对象重新抛出,那么printstacktrace()方法显示的将是原来异常抛出点的调用栈信息,而并非重新抛出点的信息。要想更新这个信息,可以调用 fillInStackTrace()方法,这将返回一个 Throwable对象,它是通过把当前调用栈信息填入原来那个异常对象而建立的。
Exception和Error有什么区别?
- https://time.geekbang.org/column/article/6849
JVM是如何处理异常的?
- https://time.geekbang.org/column/article/12134