Java的基本理念是 [构不佳的代码不能运行]
发现错误的理想时机是在编译阶段,然而,编译期间并不能找出所有的错误,余下的问题必须在运行期间解决。这就需要错误源能通过某种方式,把适当的信息传递给某个接收者–该接收者将知道如何正确处理这个问题
概念
异常这个词有 “我对此感到意外” 的意思,问题出现了,你也许不知道该如何处理,但你的确知道不该置之不理,你要停下来,看看是不是有别人或在别的地方,能够处理这个问题。只是在当前的环境中还没有足够的信息来解决这个问题,所以就把这个问题提交到一个更高级别的环境中,在这里将做出正确的决定
使用异常的另一个相当明显的好处是,它往往能够降低错误处理代码的复杂度
- 如果不使用异常,那么久必须检查特定的错误,并在程序的许多地方去处理它
- 如果使用异常,那么就不必在方法调用处进行检查,因为异常机制能够保证捕获这个错误,并且只需要在一个地方处理错误,即所谓的异常处理程序中。这种方式不仅节省代码,而且把 描述在正常执行过程中做什么事的代码和出了问题怎么办的代码相分离
基本异常
异常情形(exceptional condition)是指阻止当前方法或作用域继续执行的问题。把异常情形与普通问题区分开很重要
当抛出异常后,有几件事会随之发生
1). 使用new在堆上创建异常对象
2). 当前的执行路径被终止(不能继续下去),并从当前环境中弹出对异常对象的引用
3). 异常处理机制接管程序,并开始寻找一个恰当的地方来继续执行程序(这个恰当的地方就是异常处理程序)。异常处理程序的任务:将程序从错误的状态中恢复,以使程序能要么换一种方式运行,要么继续运行下去
异常允许我们(如果没有其他手段)强制程序停止运行,并告诉我们出现了什么问题,或者(理想状态下)强制程序处理问题,并返回到一个稳定的状态
异常参数
异常对象也是一种对象,所有标准异常类都有两个构造器
默认构造器
接受字符串作为参数的构造器
throw new NullPointerException(“t=null”)
异常的返回“地点”与普通方法调用返回的地点完全不同(异常将在一个恰当的异常处理程序中得到解决,它的位置可能离异常被抛出的地方很远,也可能会跨越方法调用栈的许多层次)
错误信息可以保存在异常对象内部,或者用异常类的名称来暗示。上一层环境通过这些信息来决定如何处理异常。(通常,异常对象中仅有的信息就是异常类型,除此之外,不包含任何有意义的内容)
捕获异常
要明白异常时如何捕获的,必须首先理解 监控区域(guarded region) 的概念
它是一段可能产生异常的代码,并且后面跟着处理这些异常的代码
Try Clause
如果在方法内部抛出了异常(或者在方法内部调用的其他方法抛出了异常),这个方法将在抛出异常的过程中结束
如果不希望此方法结束,可以在方法内设置一个特殊的块来捕获异常。
1 | try{ |
2 | // code that might generate execeptions |
3 | } |
对于不支持异常处理的语言,在每一个可能出现错误的地方,都要在调用方法的前后加上设置和检查错误的代码,程序显得很零散,而有了异常机制,可以统一处理,把所有可能出现异常的动作都放在 try 块里,然后只需要在一个地方就可以捕获所有异常。
这意味着代码更容易编写和阅读,因为完成任务的代码没有和错误检查代码混淆在一起。
异常处理程序
1 | try{ |
2 | // code that might generate execeptions |
3 | } catch(Type1 id1){ |
4 | // handle exceptipon of type1 |
5 | } catch(Type2 id2){ |
6 | // handle exception of type2 |
7 | } catch(Type3 id3){ |
8 | // handle exeception of type3 |
9 | } |
10 | // etc.... |
异常处理程序必须紧跟在try块之后,当异常被抛出的时候,异常处理机制将负责搜寻参数与异常类型相匹配的第一个处理程序。然后进入 catch 子句执行,此时认为异常得到了处理
注意:在try内可能出现多个同一类型的异常,而你只需要提供一个针对此类型的异常处理程序
当我们的程序不能够正确地处理异常(在调用的方法链中找不到处理器),程序就会终止并且在控制台打印出错误信息
比如我们常看到的日志里的 NullPointerException ,这可不是编译器帮我们处理了异常,而是你没处理导致的打印错误信息。
终止与恢复
异常处理理论上有两种基本模型
- 终止模型
- 恢复模型
创建自定义异常
不必拘泥于Java中已有的异常类型,Java提供的异常体系不可能遇见所有的希望加以报告的错误(而且对于错误的定义,不同的程序不一样),所以可以自定义异常来表示程序中可能会遇到的特定的问题。
如何自定义异常
1). 继承现有的异常类
2). 让编译器产生默认构造器 : Class SimpleException extends Exception{}编译器产生的默认构造器,将自动调用基类的默认构造器。
3). 自己定义类名,因为对于异常来说,类名真的太重要了,也可以为异常定义可以接受参数的构造器
1 | class MyException extends Exception{ |
2 | public MyException(){} |
3 | public MyException(String msg){} |
4 | } |
捕获i到异常后,会跳到处理程序,之间的代码不会执行,后面的代码可以正常执行
1 | try { |
2 | System.out.println(new ThrowTest().DividMethod(6, 0)); |
3 | Log.d(TAG, "onCreate: after exception in try block"); |
4 | }catch(Exception e){ |
5 | Log.d(TAG, "onCreate: "+e.getMessage()); |
6 | |
7 | e.printStackTrace(); |
8 | } |
9 | |
10 | // 处理了异常程序就能正确地执行下去 |
11 | Log.d(TAG, "onCreate: out of exceptions"); |
Throwable # printStackTrace()
在异常处理程序中,可以调用 Throwable 类声明的 printStackTrace() 方法。它将打印从方法调用处直到异常抛出处的方法调用序列
1 | e.printStackTrace(); // 此信息将被输出到标准错误流 |
异常说明
Java鼓励人们把方法可能会抛出的异常告知使用此方法的客户端程序员,这是种优雅的做法,它使得调用者能确切知道写什么样的代码可以捕获所有潜在的异常(当然如果提供了源代码,程序员可以在源码中查找throw语句来获知相关信息)
然而程序并不与源代码一起发布,为了预防这样的问题,Java提供了相应的语法(并强制使用这个语法),使你能以礼貌的方式告知客户端程序员,某个方法可能抛出的异常类型。这就是 异常说明
1 | void f() throws TooBig,TooSamll,DivZero{//........} |
代码必须与异常说明保持一致。如果方法里的代码产生了异常却没有进行处理,编译器会发现这个问题并提醒你,要么处理这个异常,要么就在异常说明中表明此方法将产生异常(相当于具备将它抛出的能力),通过这种自顶向下强制执行的异常说明机制,Java在编译阶段就可以保证一定水平的异常正确性。
这种在编译时强制检查的异常被称为 被检查的异常
捕获所有的异常
可以只写一个异常处理程序来捕获所有类型的异常。通过捕获异常类型的基类 Exception。
1 | catch(Exception e){ |
2 | System.out.println("catch an exception") |
3 | } |
最好把它放在处理程序列表的末尾,以防它抢在其他处理程序之前先把异常不活了
1 | catch(Type1 id1){ |
2 | |
3 | }catch(Type2 id2){ |
4 | |
5 | }catch(Exception e){ |
6 | |
7 | } |
About printStackTrace()
首先,它不是来自Exzception类。Exception本身除了定义了几个构造器之外,所有的方法都是从其父类继承过来的。
其次,我们应该注意到的是它所打印的是什么东西
1 | public class TestPrintStackTrace{ |
2 | public static void f() throws Exception{ |
3 | throw new Exception("出问题拉"); |
4 | } |
5 | |
6 | public static void g() throws Exception { |
7 | f(); |
8 | } |
9 | |
10 | public static void main(String[] args){ |
11 | try{ |
12 | g(); |
13 | }catch(Exception e){ |
14 | e.printStackTrace(); |
15 | } |
16 | } |
17 | } |
18 | ------------------------------------------------ |
19 | java.lang.Exception:出问题啦! |
20 | at TestPrintStackTrace.f(TestPrintStackTrace.java:3) |
21 | at TestPrintStackTrace.f(TestPrintStackTrace.java:6) |
22 | at TestPrintStackTrace.f(TestPrintStackTrace.java:10) |
打印的是抛出异常的位置到捕获异常的位置的调用链(逆向打印:实际调用链的逆向)
Java标准异常
Throwable 这个Java类被用来表示任何可以作为异常被抛出的类,它可以分为两种类型
- Error : 表示编译时和系统错误
- Exception :可以被抛出的基本类型。
- 在Java类库、用户方法以及运行时故障中都可能抛出 Exception 型异常,所以Java程序员关心的基本类型通常是 Exception
异常的基本概念是用名称代替发生的问题,并且异常的名称应该可以望文知意。异常也并非都是定义在 java.lang 包里的。
特例:RuntaimeException
何为运行时异常?
比如,NullPointerException,如果必须对传递给方法的每个引用都检查其是否为null(因为无法确认调用者是否能传入了非法引用),这听起来很吓人。幸运的是,这不需要你来做,它属于 Java 的标准运行时检测的一部分。如果对 null 引用进行调用,Java 会自动抛出 NullPointerException ,属于运行时异常很多,他们会自动被Java虚拟机抛出,所以不必在异常说明中把他们列出来。不过尽管通常不用捕获 RuntimeException ,但是还是可以在代码中抛出 RuntimeException
如果 RuntimeException 没有被捕获而直达 main() ,那么在程序退出前将调用异常的 printStackTrace() 方法
1 | public class MainActivity{ |
2 | |
3 | protected void onCreate(){ |
4 | reTest(); |
5 | } |
6 | |
7 | private void reTest(){ // 不需要在异常说明里标注,JVM会自动抛出这个异常,程序终止 |
8 | throw new MyRuntimeException(); |
9 | } |
10 | |
11 | class MyRuntimeException extends RuntimeException{ } |
12 | } |
13 | |
14 | ---------------------------------------------------------------------------------- |
15 | java.lang.RuntimeException: Unable to start activity ComponentInfo{com.example.exceptionapp/com.example.exceptionapp.MainActivity}: com.example.exceptionapp.MainActivity$MyRuntimeException |
16 | at android.app.ActivityThread.performLaunchActivity(ActivityThread.java:2913) |
17 | at android.app.ActivityThread.handleLaunchActivity(ActivityThread.java:3048) |
18 | at android.app.servertransaction.LaunchActivityItem.execute(LaunchActivityItem.java:78) |
19 | at android.app.servertransaction.TransactionExecutor.executeCallbacks(TransactionExecutor.java:108) |
20 | at android.app.servertransaction.TransactionExecutor.execute(TransactionExecutor.java:68) |
21 | at android.app.ActivityThread$H.handleMessage(ActivityThread.java:1808) |
22 | at android.os.Handler.dispatchMessage(Handler.java:106) |
23 | at android.os.Looper.loop(Looper.java:193) |
24 | at android.app.ActivityThread.main(ActivityThread.java:6669) |
25 | at java.lang.reflect.Method.invoke(Native Method) |
26 | at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:493) |
27 | at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:858) |
当然你也可以用 try-catch将运行时异常捕获,这样程序不会终止
RuntimeException 代表的是编程错误:
- 无法预料的错误,比如从你控制范围之外传递进来的 Null 引用
- 作为程序员,应该在代码中进行检查的错误(比如 ArrayIndexOutOfBoundsException,就得注意一下数组的大小了)
Java为何把异常分为受检查异常和运行时异常?
这是一个非常关键的问题,只有理解了这个,那么你才能从整体上对异常有个清醒的认识,也知道如何更好地去使用异常机制
从设计思想上来说,当出现运行时异常的情况下,你什么也不能做,当然,并不是系统不让你做,而是你无能为力,比如出现了空指针
使用 finally 进行清理
对于一些代码,可能会希望无论 try 块中的异常是否被抛出,他们都能得到执行。
这通常适用于内存回收之外的情况(因为回收由辣鸡器完成),finally可以达到这个效果
1 | try{ |
2 | |
3 | }catch(){ |
4 | |
5 | }finally{ |
6 | // Activities that happen every time |
7 | } |
tip:利用while语句和finally子句结合可以使得不停的执行try块,直到程序达到所需的条件,是一个提高程序健壮性的思路
finally用来做什么?
对于没有垃圾回收和析构函数自动调用机制的语言来说,finally非常重要。它能使程序员保证:无论try块里发生了什么,内存总能得到释放
Java有垃圾回收机制,所以内存释放不是问题,那么,Java在什么情况下用到 finally 呢?
当需要把内存之外的资源恢复到它们的初始状态时,就要用到 finally 子句。这种需要清理的资源包括:已经打开的文件或者网络连接,在屏幕上画的图形,甚至可以是外部世界的某个开关,我们来看一下不用finally需要如何来处理:
1 | .... |
2 | // sw 是对资源的抽象 |
3 | public static void main(String[] args){ |
4 | try{ |
5 | sw.on(); |
6 | f(); // code that can throw exception |
7 | sw.off(); |
8 | }catch(Exception1 e1){ |
9 | // do sth |
10 | sw.off(); |
11 | }catch(Exception2 e2){ |
12 | //do sth |
13 | sw.off(); |
14 | } |
15 | } |
为了避免资源没有被关闭,恢复到 on 的状态,我们需要在不同的地方都写上 off 代码,而finally则不一样。它可以保证一定执行,无论有没有抛出异常,也无论该异常有没有被当前的catch从句集里捕获,finally 都会在异常控制机制转到更高级别搜索一个控制器之前的一致性。
1 | try{ |
2 | sw.on; |
3 | f(); |
4 | }catch(Exception1 e1){ |
5 | //do sth |
6 | }catch(Exception2 e2){ |
7 | // do sth |
8 | }finally{ |
9 | sw.off(); |
10 | } |
啊哈!简洁而逻辑清晰。
T:C++异常控制未提供 finally 从句,因为它依赖构造器(析构函数?)来达到这种清除效果
缺点:丢失的异常
Exception Losing
异常丢失的本质原因是一个方法一次只能抛出一个异常
我想类似于下面这样的
1 | int exeception; // 你可以理解为exception就是记录这个方法所抛异常的变量 |
2 | exception = old; |
3 | .... |
4 | exception = new; |
5 | return exception; |
当然普通情况下,是不会有异常丢失的,因为一旦方法内部抛出一个异常,要么意味着它在内部被捕获,要么就终止下面的代码的执行转而回溯上层寻找catch clause,既然后面的代码没有机会 excute,那么自然不会抛出新的异常。但是,Java的finally机制则给予了异常丢失的机会,因为它能够在抛出异常(还未抛出方法,仅在内部抛出)后还能得以运行,如果在finally内部再抛出一个异常,就会将之前的异常覆盖,从而造成了异常丢失。
1 | void f() { |
2 | try{ |
3 | throw new RuntimeException(); |
4 | }finally{ |
5 | throw new Exception(); |
6 | } |
7 | } |
Try-catch-finally flow
首先,我们来看 try-catch,try后面不一定要跟catch,但是catch和finally至少有一种。
有以下几种情况,有一个需要注意的地方是,这里的catch并不一定是紧跟着try的catch,他有可能距离异常抛出点很远,处于调用栈的上游。
但是无论是何种租和方式,catch的层级在方法内外,本质上,都是一样的。
就是基于程序流程来进行(从上至下),执行完毕后,如果有Exception未处理,则开始回溯。也就是说在finally执行完毕之前,异常并不会抛出到方法外部,注意这个先后顺序。
当然,这里有一种特殊情况,就是clause中存在流程控制语句,如return等,这种则要具体分析
例如,finally中的return语句会覆盖别处的return语句(如果确保finally能够执行的情况下),当然还有很多种情况的组合,原则就是 finally 一定会调用,而且会在 return 发生之前调用。
Finally did not work.
finally就一定会执行么?在以下两种情况下,finally是不会执行的:
1) try语句没有被执行到
2) 例如调用 System.exit(0) 退出 JVM
Constructor
Exception mactch
“抛出”一个异常后,异常控制系统会按照当初编写的顺序搜索”最接近”的控制器。一旦找到相符的控制器,就认为异常已得到控制,不再进行更多的搜索工作。
如果将基类捕获从句置于第一位,试图屏蔽派生类异常,则编译器会产生一条出错消息,因为它发现永远不可能抵达 Sneeze 捕获从句
Restriction
覆写一个方法时,只能产生已在方法的基类版本中定义的异常。这是一个重要的限制,因为它意味着与基类协同工作的代码也会自动应用于从基类派生的任何对象(当然,这属于基本的OOP概念)
Exception Princeple
用异常做下面这些事情,并按照异常的严重情况逐级递增
1) 解决问题并再次调用造成异常的方法
2) 平息事态的发展,并在不重新尝试方法的前提下继续
3) 计算另一些结果,而不是希望方法产生的结果
4) 在当前环境中尽可能解决问题,以及将相同的异常重新抛向一个更高级的环境
5) 中止程序执行
Crash Info Collection
有很多类似的第三方,比如友盟、Bugly等等;网络上也有一些免费的开源的三方,比如ACRA ;当然,我们也可以通过实现 UncaughtExceptionHandler接口来实现类似以上的功能.其实很简单,我们主要了解一下UncaughtExceptionHandler 这个接口就可以了
1 | public static interface UncaughtExceptionHandler { |
2 | void uncaughtException(Thread thread, Throwable ex); |
3 | } |
这是一个 Thread的静态内部类 Thread.UncaughtExceptionHandler。一个接口具备的含义即它具备了什么样的能力,而该接口的能力就是处理那些未被捕获的异常,我们只要实现这个接口,然后,再通过下面这个方法为线程对象设置这个处理类。这实际上是一种策略模式。
1 | public void setUncaughtExceptionHandler(UncaughtExceptionHandler handler) { |
2 | uncaughtHandler = handler; |
3 | } |
Reference
References
[02.异常分类的依据](at TestPrintStackTrace.f(TestPrintStackTrace.java:3))
04.why NullPointerException is defined as a RuntimeException