前言
A clever person solves a problem, a wise person avoids it.
——那你,会如何处理异常,try or throw?
1 异常是什么
我有一辆车(不是自行车,四轮的!!诶,更不是拖拉机!!!),我要开车去隔壁村见小王(不是隔壁老王),开到半路爆胎了,此时,我有两个选择:
- 如果我车上有备胎(诶,可惜我备胎这东西是建立在有女朋友的基础之上的,女朋友是个好东西,然而我并没有),我可以选择在当前就换个轮胎,然后将坏了的车胎放到后备箱,车子仍然可以继续开;
- 我车上没有备胎,那我只能放个警示牌,打个电话给能处理这个车爆胎的人来处理,而这并不妨碍我走着去见隔壁村的小王。
这就是在Java中遇到异常的两个常用的方法,你可以处理它(try)或抛出它(throw),即将锅交给别人去处理。
“异常”这个词通常有“我对此感到很意外”的意思。问题出现了,你也许不清楚该如何处理,但你的确知道不应该置之不理;你要停下来,看看是不是有别人或在别的地方,能够处理这个问题。只是在当前的环境中,还没有足够的信息来解决这个问题,所以就把这个问题提交到一个更高级别的环境中,在那里将做出正确的决定。
——《Java编程思想》 P248
异常指不期而遇的各种状况,如:文件找不到、网络连接失败,非法参数等。异常是一个事件,它发生在程序运行期间,干扰了正常的指令流程。Java通过API中Throwable类的众多子类描述各种不同的异常。因而,Java异常都是对象,是Throwable子类的实例。Java异常类的层次结构图如下:
- Throwable:Java语言中所有错误或异常的超类,有两个重要的子类,Exception(异常)和Error(错误),二者都是Java异常处理的重要子类,各自都包含大量子类。
- Error:表示仅靠程序本身无法恢复的严重错误。与代码编写者无关,是代码运行时JVM出现的错误,发生于虚拟机自身、或者发生在虚拟机试图执行应用时,如Java虚拟机运行错误(VirtualMachineError)、类定义错误(NoClassDefFoundError)等。这些错误都是不可查的,因为他们在应用程序的控制和处理能力之外,而且绝大多数是程序运行时不允许出现的状况。
- Exception:表示程序本身可以处理的异常。
- RuntimeException:那些可能在Java虚拟机正常运行期间抛出的异常的超类。Java编译器不去检查它,也就是说,当程序中可能出现这类异常时,即使没有用
try...catch
语句捕获它,也没有用throws
子句声明抛出它,也还是会编译通过。例如:若试图使用空值对象引用、除数为零或数组越界,则分别引发运行时异常(NullPointerException/ArithmeticException/ArrayIndexOutOfBoundException)。 - CheckedException:正确的程序在运行中,很容易出现的、情理可容的异常状况,可查异常状况虽然是异常状况,但在一定程度上它的发生是可以预计的,而且一旦发生这种异常状况,就必须采取某种方式进行处理。即Java编译器会检查它,也就是说,当程序中可能出现这类异常,要么用
try-catch
语句捕获它,要么用throws字句声明抛出它,否则编译不会通过。如IOException/SQLException等。
- RuntimeException:那些可能在Java虚拟机正常运行期间抛出的异常的超类。Java编译器不去检查它,也就是说,当程序中可能出现这类异常时,即使没有用
PS:Java中RuntimeException这个类名起的并不恰当,因为任何异常都是运行时出现的。(在编译时出现的错误并不是异常,换句话说,异常就是为了解决程序运行时出现的的错误)。
PPS:如何区分,如果出现RuntimeException,那么一定是程序员的错误。例如,可以通过检查数组下标和数组边界来避免数组越界访问异常。 非RutimeException一般是外部错误,例如试图从文件尾后读取数据等,这并不是程序本身的错误,而是在应用环境中出现的外部错误。
2 异常处理机制
异常机制是指当程序出现错误后,程序如何处理。具体来说,异常机制提供了程序退出的安全通道。当出现错误后,程序执行的流程发生改变,程序的控制权转移到异常处理器。在Java应用程序中,异常处理机制为:抛出异常、捕获异常。异常处理的流程:
- 遇到错误,方法立即结束,并不返回一个值;同时,抛出一个异常对象 。
- 调用该方法的程序也不会继续执行下去,而是搜索一个可以处理该异常的异常处理器,并执行其中的代码。
2.1 try-catch
抛出异常: 当遇到异常情形时,程序无法继续向下执行,因为在当前的环境下无法获得必要的信息来解决问题。只能从当前环境跳出,将问题提交至上一级环境。当抛出异常后,会有几件事情随之发生:
- 将使用
new
在堆上创建异常对象。 - 当前的执行路径(它不能继续下去了)被终止,并且从当前环境中弹出对异常对象的引用。
- 异常处理机制接管程序,并开始寻找一个恰当的地方来执行程序。这个恰当的地方就是异常处理程序,它的任务是将程序从错误状态中恢复,以使程序要么换一种方式运行,要么继续运行下去。
- 将使用
捕获异常: Java中,异常的捕获通常通过
try-catch
或try-catch-finally
语句实现,即监控区域(一段可能产生异常的代码+处理这些异常的代码)- try块:
如果在方法内部抛出了异常,这个方法将在抛出异常的过程中结束。要是不希望方法就此结束,可以在方法内设置一个特殊的块来捕获异常。因为在这块里,尝试了各种可能产生异常的方法的调用,所以称为try块。它是跟在try关键字之后的普通程序块:
- try块:
|
|
- catch子句:
抛出的异常必须在某处得到处理。这个地点就是异常处理程序,而且针对每个要捕获的异常,得准备相应的处理程序。异常程序紧跟在try块之后,以关键字catch表示:
|
|
每个catch子句看起来就像是接收一个且仅接受一个特殊类型的参数的方法。当异常被抛出后,异常处理机制将负责搜寻参数与异常类型相匹配的第一个处理程序,然后进入catch子句之中执行。只有匹配的catch子句才能得到执行。
异常匹配的原则是:如果抛出的异常对象属于catch子句的异常类,或者该异常类的子类,则认为生成的异常类对象与catch块捕获的异常类型相匹配。
下面举个例子:
捕获throw语句抛出的“除数为0”异常
|
|
运行结果:
变量b不能为0
程序正常结束
事实上,“除数为0”等ArithmeticException,是RuntimeException的子类,而运行时异常将由运行时系统自动抛出,不需要使用throw语句。故可将代码改成如下形式:
运行结果:
变量b不能为0
程序正常结束
由于检查运行时异常的代价远大于捕捉异常所带来的益处,Java编译器允许忽略运行时异常,一个方法可以既不捕捉,也不声明抛出运行时异常。如下代码所示:
运行结果:
Exception in thread “main” java.lang.ArithmeticException: / by zero
2.1.1 catch子句注意事项
- 一旦某个catch子句捕获到匹配的异常类型,将进入异常处理代码。一经处理结束,就意味着整个
try-catch
语句结束。其他的catch子句不再有匹配和捕获异常类型的机会。 - Java通过异常类描述异常类型,对于有多个catch子句的异常程序而言,应该尽量将捕获底层异常类的catch子句放在前面,同时尽量将捕获相对高层的异常类的catch子句放在后面。否则,捕获底层异常类的catch子句可能会被屏蔽。
- RuntimeException异常类包括运行时各种常见的异常,ArithmeticException类和ArrayIndexOutOfBoundsException类都是它的子类。因此,RuntimeException异常类的catch子句应该放在最后面,否则可能会屏蔽其后的特定异常处理或引起编译错误。
2.2 try-catch-finally
try-catch
语句还可以包括第三部分,就是finally子句。它表示无论是否出现异常,都应该执行的内容,try-catch-finally
语句的一般语法形式如下:
|
|
对于一些代码,可能会希望无论try块中的异常是否抛出,它们都能得到执行。图示如下:
这通常适用于内存回收之外的情况(因为回收由垃圾回收器完成)。为了达到这个效果,可以在异常处理程序后面架上finally子句。如下例所示:
|
|
2.2.1 finally子句注意事项
- 对于没有垃圾回收和析构函数自动调用机制的语言来说,finally非常重要。它能使程序员保证:无论try块里发生了什么,内存总能得到释放。但Java有垃圾回收机制,所以内存释放不再是问题。而且,Java也没有析构函数可供调用。在Java中,用到finallyde情况主要是在,当要把除内存之外的资源恢复到它们的初始状态时。这种需要清理的资源包括:已经打开的文件或网络连接,在屏幕上画的图形,甚至可以是外部世界的某个开关。
- 当在try块或catch块中遇到return语句时,finally语句块将在方法返回之前被执行。
- finally语句块不能给变量赋新值来改变return的返回值,也不建议在finally块中使用return语句。没有意义还容易造成混淆。
2.3 throw/throws
任何Java代码都可以抛出异常,如:自己编写的代码、来自Java开发环境包中的代码,或者Java运行时系统。Java对于处理不了的异常或者要转型的异常,一般用throw/throws语句抛出异常。如果一个方法没有捕获一个检查性异常,那么该方法必须使用throws关键字来声明。throws关键字放在方法签名的尾部,也可以使用throw关键字抛出一个异常,无论它是新实例化的还是刚捕获到的。
2.3.1 throws
如果与一个方法可能会出现异常,但没有能力处理这种异常,可以在方法声明处用throws子句来声明抛出异常。就好像,初中时经常会有打架时间,几个人打一个人,这个人被打之后,想报仇又无力报仇,所以叫一帮兄弟帮他报仇。
throws语句用在方法定义时声明该方法要抛出的异常类型,如果抛出的是Exception异常类型,则该方法被声明为抛出所有de异常;多个异常用逗号分隔,throws语句的语法格式如下:
|
|
throws后为声明要抛出的异常列表。当方法抛出异常列表中的异常的时候,方法将不对这些类型及其子类型进行异常处理,而将异常抛向调用该方法的方法。
举例如下:
|
|
pop方法没有处理异常NegativeArraySizeException,而是由main函数来处理。
throws抛出异常的规则:
- 如果是不可检查异常(Unchecked Exception),即Error、RuntimeException或它们的子类,那么可以不使用throws关键字来声明要抛出的异常,编译仍然能通过,但运行时会被系统抛出。
- 必须声明方法可抛出的任何可查异常(Checked Exception),即如果一个方法可能出现可查异常,要么用try-catch语句捕获,要么用throws语句声明将它抛出,否则会导致编译错误。
- 该方法的调用者必须处理或者重新抛出该异常。当方法的调用者无力处理该异常的时候,应该继续抛出,而不是囫囵吞枣。
- 调用方法必须遵循任何可查异常的处理和声明规则。若覆盖一个方法,则不能声明与覆盖方法不同的异常。声明的任何异常必须是被覆盖方法所声明异常的同类或子类。
2.3.2 throw
throw
总是出现在函数体中,用来抛出一个Throwable的异常,程序会在throw语句后立即终止,它后面的语句总是执行不到的。
异常是异常类的实力对象,我们可以创建异常类的实例对象通过throw语句抛出,该语句的语法格式如下:
如果抛出了检查异常,则还应该在方法头部声明方法可能抛出的异常类型。该方法的调用者也必须检查处理抛出的异常,如果所有方法都层层上抛获取的异常,最终JVM会进行处理。处理方式也很简单,即打印异常消息和堆栈消息。
2.3.3 Throwable类中的常用方法
catch关键字后面括号中的Exception类型的参数e。Exception就是try代码块传递给catch代码块的变量类型,e就是变量名。通常异常处理常用3个函数来获取异常的有关信息:getCause()
: 返回抛出异常的原因。如果 cause 不存在或未知,则返回 null。getMeage()
: 返回异常的消息信息。printStackTrace()
: 对象的堆栈跟踪输出至错误输出流,作为字段 System.err 的值。
有时为了简单会忽略掉catch语句后的代码,这样try-catch语句就成了一种摆设,一旦程序在运行过程中出现了异常,就会忽略处理异常,而错误发生的原因很难查找。
3 自定义异常
所谓自定义异常,通常就是指定义了一个继承自Exception类的子类,那么这个类就是一个自定义异常类。通常情况下,我们都会继承自Exception类,一般不会继承某个运行时的异常类。基于特定的需求,自定义异常在项目中的使用还是很普遍的。
自定义异常类MyException:
|
|
当然也可选用Throwable作为父类。其中无参数构造器为创建缺省参数对象提供了方便。第二个构造器将在创建这个异常对象时提供描述这个异常信息的字符串,通过调用父类构造器向上传递给父类,对父类中的toString()方法中返回的原有信息进行覆盖。作为该异常的异常信息。
测试自定义异常类代码:
1.抛出异常throws:
运行结果:
Exception in thread “main” MyException: 传入的字符串参数不能为null
at ExxceptionTest.method(ExxceptionTest.java:9)
at ExxceptionTest.main(ExxceptionTest.java:29)
……
2.处理异常try-catch:
运行结果:
MyException: 传入的字符串参数不能为null
at ExxceptionTest.method(ExceptionTest.java:9)
at ExxceptionTest.main(ExceptionTest.java:19)
….
异常处理完毕
程序执行完毕
毋庸置疑,我们不可能期待JVM自动抛出一个自定义异常,也不能够期待JVM会自动处理一个自定义异常。发现异常、抛出异常以及处理异常的工作必须靠编程人员在代码中利用异常处理机制自己完成。而打印异常处理信息可以在抛出时包括在构造器的参数中,或者包括在处理这个异常的catch中。
不定期更新…