java 异常
八月的第四周,来学习 Java 的异常处理机制。
异常就是不正常,程序不能正常执行,中途遇到了的问题就是异常。这次来写异常分哪些情况,以及怎么处理异常。
首先需要厘清的是,几乎所有介绍异常的总结贴,以及面试时对异常的提问,基本上说的都不光是异常,而是整个异常体系。
看一下简图就很明白了:父类 Throwable 有两个子类,一个是 Error (错误),另一个才是 Exception (异常)。也就是说,异常体系说的不光是异常,还把他爸和他哥一起介绍了,一家包圆了。
光从词语的表达上就能感受到,Error 和 Exception 的严重程度是不同的,Error (错误)是非常严重的事情,而 Exception (异常)是可以商量的事情。
- Error :错误,指的是程序中很严重的问题,严重到 Java 虚拟机都无法处理,只能停止程序改bug。
- Exception :异常,指的是程序中的轻度问题,程序运行时遇到了之后,处理一下还可以继续执行下去(当然了,一定要处理,不能随便放任不管)。
上面那张图只是一张表示 Error 和 Exception 之间关系的图,实际上的异常体系还是比较庞大的,我找到了一张稍微全一些的体系图(图片来源)。
现在你要注意到,上图中的所有东西,比如 Error
,比如 Exception
,比如 Exception
里的 NullPointerException
,不论是错误也好异常也罢,以上的所有东西,统统都是类。Java 作为一门面向对象的语言,已经连错误、异常这些东西,都封装起来看作是一种类了。上图中的所有东西都是类,都有继承关系,都有方法,都是当做一个个的封装起来的对象来处理的。
Error
Error (错误)不是这周学习的重点,事实上其内容需要好几年 Java 实战的积累才能逐渐掌握,因为里面涉及到的具体情况都是偏硬件、偏底层的东西,比如 OutOfMemoryError
(内存溢出错误类)。
我们目前只需要知道,Java 虚拟机并不会处理 Error 类,也就是说编译器不会检查 Error 类,我们在做程序设计的时候,也不应该去尝试捕获 Error 类。
Exception
Exception (异常)是这周关注的重点。
Exception 有许许多多的子类,其中一类 RuntimeException
即运行时异常
,这个子类和其他子类有所不同,怎么不同的一会再说。总之 Exception 可以分成两类,一类叫做运行时异常
,另一类叫做非运行时异常
。
怎么不同的呢?
RuntimeException
运行时异常,就是程序真的运行起来了,才能发现的异常。- 非运行时异常,意思是说,程序即使不运行,编译器都知道将会有异常发生。也就是说,你不处理这个异常,你就别想让程序运行起来。
这两类异常的主要区别在于,一种程序可以先运行起来(遇到了再报错),一种程序上来就无法运行(不用遇到了,直接报错)。
再换句话说,一种可以通过编译器的检查,一种无法通过编译器的检查。
如果从编译器检查的角度来看这两类异常,那么运行时异常( RuntimeException ),因为可以逃避编译器的检查,因此可以被称为非检查性异常;而其他异常(也就是非运行时异常)逃不过编译器的检查,因此可以被称为检查性异常。
所以异常可以换一种分类方式:检查性异常
、非检查性异常
。这一种分类方式是更被普遍提到的,虽然它所指的其实就是运行时和非运行时,只是看待的角度不同。(现在建议你再去看一看上面的图,再理解一下分类)
非检查性异常/运行时异常
属于这一类的异常中,较为常见的有:
NullPointerException
空指针异常ArithmeticException
算术异常(比如 5 除以 0 )ClassCastException
类型转换异常IndexOutOfBoundsException
数组越界异常ArrayStoreException
数据存储异常(数组存储时数据类型不一致)
检查性异常/非运行时异常
属于这一类异常中,较为常见的有:
IOException
输入输出异常FileNotFoundException
文件不存在异常(继承自IOException
)SQLException
SQL 语句异常InterruptedException
中断异常
现在来学习,如果遇到了异常( Exception ),怎么处理异常。
准确的说应该是,我怀疑某个地方要出现异常,我怎么提前处理这个地方,让它在程序运行之后真的遇到了异常时,仍然能在处理之后继续运行程序。
要注意的是,检查性异常是一定要进行处理的,不处理程序都无法运行(因为编译器直接报错),而非检查性异常和没有异常的地方(比如合理怀疑某段代码有问题),也是可以进行异常处理的。异常处理并不是只针对于异常的,而是在一切场景之下,只要我怀疑存在异常,都可以去做异常处理(大不了没碰上呗)。
Java 有两种异常处理方案,分别是就地解决
( try / catch / finally 语句)和异地正法
( throws )。
就地解决
现在我遇到一段可能存在异常的代码,打算当场解决掉它。
下面这几行代码就是就地解决异常的处理方式。
1 | try { |
有三个关键字:
try
大括号里放【可能存在异常的代码】,表示“这段代码可能有异常,我们来试试看”。catch
小括号里放【异常的类型】,大括号里放【处理异常的代码】,表示“我发现了XX异常,我要这么去处理它”。finally
大括号里放【不论如何都会执行的代码】,表示“我管你遇不遇得到异常,全都给我执行这段代码”。
还是举一个具体的例子吧。
1 | try { |
- 程序进入
try{}
语句块中,执行了int number = 1/0;
这行代码,因为 0 不能作为被除数,所以出现了异常。 - 通过
catch(){}
语句块捕捉到了ArithmeticException
(算术异常),你应该注意到了,小括号里放的就是这一异常的类型,如果放的是别的异常的话,那么会捕捉不到的。大括号里表示,捕捉到这一异常之后,要执行的操作,在这里是在控制台打印了一句话:“发现了算术异常”。 finally{}
语句块是程序无论如何都会执行的部分,即使是没有异常也会执行。在这里发生了算数异常,打印了一句话:“不论如何,都给我执行这段代码”。其实如果没有发生异常的话,也会把这句话打印出来的。
异地正法
有时我们并不想在异常出现的地方就去处理它,那么可以把异常抛出去
。
抛出去(throw)又是一个翻译过来的词,在英文环境中这个词很生动,但是在汉语环境中就并不如此。我们经常听到抛异常
这三个字,意思就是把异常给“抛”出去,不立即处理,而是在其他地方去处理。
比如现在有方法 A
,方法 A
调用了方法 B
,方法 B
的内部出现了异常,现在就有两种解决方案:
方法 B
自己处理异常,也就是刚才说的“就地正法”。方法 B
不去解决,而是“抛”给方法 A
去解决。
为什么不当场解决,而是抛出去让其他地方去解决,原因可能有很多,比如另一个地方拿到异常对象能做更多事情,比如程序员懒想拖到其他地方再说,等等。
抛异常
的动作很简单,只需要在方法名的后面(注意,不是在异常发生的原地,而是在异常发生所在的方法,那个方法的名字的位置处),加上 throws XXXException
就可以了。
举个栗子吧:
1 | private int test() throws ArithmeticException { |
这里定义了一个只有两行代码的方法(方法名叫 test
),第一行是让 number = 1/0
(很明显出现了算术异常),第二行返回 number
。我并没有在异常出现的原地去处理,而是在方法名的后面,加上了 throws ArithmeticException
,表示抛出去了一个算术异常。之后哪里调用了这个方法,就必须去处理这个异常(或者继续抛出去)。
你注意,这里说的抛异常,是我们遇到了异常之后,把遇到的这个异常抛出去,而且是在方法名旁边去抛的,意思是说“每当你调用这个方法的时候,都要注意,这个方法里面有个异常要处理”。但是还有一种抛异常的方法,就是可以在随便一个地方,手动地抛出去一个异常。
1 | throw new ArithmeticException(); |
比如这个样子,我手动地把一个新实例化出来的算术异常抛出去了。
在方法名旁边写的那个,是 throws
,而手动在随便一个地方抛出去的,是 throw
。
剩下的部分太过零碎,我就分条写,写到哪里算哪里好了。
1.finally
语句块
在捕获异常当场解决的地方,我们使用了 try / catch / finally 语句,其中 finally{}
语句块是“无论如何都会执行的代码”。
这里的无论如何,真的就是无论如何,它保证了程序一定会执行这部分代码,不管发不发生异常都会执行,而且不管 try{}
语句块中写了什么,也都会执行。
注意:即使是 try{}
语句块中写了返回语句 return xxx;
,你感觉程序走到这里就停止了,其实并不是,这样也都会去执行 finally{}
语句块。
1 | try { |
通过反编译可以发现,Java 在处理 finally{}
语句块时,是把它复制成两份,分别在 try{}
语句块和 catch{}
语句块后面去执行一次的。
而且如果 finally{}
语句块中写了返回语句 return xxx;
,将会执行这个返回语句,程序到此结束。
2.异常链
维基百科对于异常链的解释真是太令人舒服了,一眼看懂:
异常链是一种面向对象编程技术,指将捕获的异常包装进一个新的异常中并重新抛出的异常处理方式。原异常被保存为新异常的一个属性(比如cause)。这个想法是指一个方法应该抛出定义在相同的抽象层次上的异常,但不会丢弃更低层次的信息。
还是举一个例子吧:
main
方法调用 test1
方法,test1
方法调用 test2
方法。
其中 test2
方法中抛一个异常, test1
方法会接到 test2
方法抛出的异常,包裹起来再抛一个新的异常。
这样 main
方法中就会出现一个异常,这个异常里面还有一个异常(也就是接到一个包裹着异常的异常)。
1 | public class Test { |
控制台的打印信息如下:
1 | java.lang.Exception: 这里是test2的异常 |
这里就看到了异常链。
3.一个catch子句捕获多个异常
JDK1.7
改进了 catch 子句的语法,允许在其中指定多种异常,每个异常类型之间使用“|”来分隔。
1 | public class ExceptionHandler { |
4.异常抑制
JDK1.7
中为 Throwable类
增加 addSuppressed方法
和 getSuppressed方法
。
当一个异常被抛出的时候,可能有其他异常因为该异常而被抑制住,从而无法正常抛出。这时可以通过 addSuppressed方法
把这些被抑制的方法记录下来,被抑制的异常会出现在抛出的异常的堆栈信息中,可以通过 getSuppressed方法
获取到被抑制的异常。
个人觉得没啥用,懒得写了。
5. try - with - resources
同样还是 JDK1.7
增加的新特性。
以前使用 try / catch / finally
语句块处理资源的时候,如果要关闭资源,需要在 finally{}
语句块中关闭,使用 try - with - resources
方式可以在 try()
的小括号里声明,Java 会自动关闭资源。
1 | BufferedReader br = new BufferedReader(inputString); |
JDK1.9
更进一步, try()
的小括号里不需要声明了,声明过程在外面也可以。
1 | BufferedReader br = new BufferedReader(inputString); |
try 后面小括号内的东西即为 resources ,resources 必须是实现 java.lang.AutoCloseable
或者 java.io.Closeable
接口的类(需要手动 close() 释放资源的)。
6.IDEA 的快捷操作
要是使用的Java开发工具是 IDEA 的话,那么如果想对某一句代码进行异常处理,可以在这句代码写完之后,敲上“ .try
“这四个字符,再敲回车,就可以自动为这行代码加上 try / catch 语句了。
例如这行代码:
1 | int number = 1/0;.try |
写完之后敲击回车,自动变成如下代码:
1 | try { |