本文介绍Java中的异常机制。
1. 异常概述
程序在编写或者运行过程中会发生一些不正常的情况,比如除数为0,类型不能转换等情况。这种不正常的情况叫做:异常。
Java语言是很完善的语言,提供了异常的处理方式。当异常出现后,程序停止执行,JVM把该异常信息打印输出到控制台,供程序员参考。程序员看到异常信息之后,可以对程序进行修改,让程序更加的健壮。
在API官方文档中可以看到,java.lang
包下提供了很多的异常类,如NumberFormatException、ClassCastException。所以异常是以类和对象的形式存在的。程序运行过程中如果遇到不正常情况,JVM会创建对应的异常对象,并将对象信息打印到控制台上。简单代码如下所示:
1 | public class ExceptionTest01 { |
2. 异常的继承结构
根类是Object类,然后Throwable类继承Object类,是所有错误(Error)和异常(Exception)的父类。不管是错误还是异常,都是可抛出的。
Throwable类有Error和Exception两个直接子类。
错误只要发生,Java程序只有一个结果,就是终止程序的执行,退出JVM,错误是不能处理的。而异常允许处理,不处理也会终止执行。
异常分为编译时异常和运行时异常,异常Exception类的直接子类均为编译时异常,编译时异常并不是指的是在编译阶段发生的异常,而是表示必须在编写程序的时候预先处理这种异常,如果不处理编译器会报错。除了直接子类还有RuntimeException类运行时异常,在编写阶段可以处理也可以不处理。
2.1 编译时异常和运行时异常
编译时异常和运行时异常,都是发生在运行阶段,编译阶段是不会发生的。编译时异常必须在编译(编写)阶段预先处理,如果不处理编译器会报错,所以称为编译时异常。所有异常都是在运行阶段发生的,因为只有程序运行阶段才可以new对象,异常的“直观体现”就是new异常对象。
编译时异常一般发生的概率比较高,而运行时异常一般发生的概率比较低。编译时异常一般是可以预料到的,发生概率较高的,比如淋雨会导致生病,在运行前对其进行预处理(提前拿一把伞);而运行时异常则是不可预料到的,比如天降横祸,在运行前没必要对这种异常进行处理。
2.2 异常的处理方式
Java语言对异常的处理包括两种方式:
- 在异常所在方法声明的位置上,使用throws关键字,称为抛出/上抛,注意抛出的异常范围不能比当前异常小,就是不能抛其子类异常。
- 使用try…catch语句进行异常的捕捉,称为捕捉。
注意,只要发生异常,如果采用throws上抛,那么异常语句之后的语句就不会执行;如果采用try…catch捕捉,那么try括号里面的异常语句之后的语句不会执行。
try…catch语法如下,注意,捕捉只能捕捉指定的异常及其子类型异常,其他类型异常不会捕捉到:
1 | try{ |
注意,异常发生后,如果选择了上抛,即抛给了方法的调用者,那么调用者需要对这个异常继续处理,同样有这两种处理方式。
注意,Java中异常发生之后如果一直上抛,最终抛给了main方法,main方法继续向上抛,抛给了调用者JVM,JVM知道这个异常发生,只有一个结果:终止Java程序的执行。
一般不建议在main方法上使用throws,因为这个异常如果真正的发生了,一定会抛给JVM,JVM只能终止程序。而异常处理机制的作用就是增强程序的健壮性,让程序不终止。所以一般main方法中的异常建议使用 try…catch 进行捕捉,main方法就不要继续上抛了。
简单案例如下所示:
案例1,运行时异常,不需要提前处理。
1 | public class ExceptionTest01 { |
案例2,,编译时异常,不处理编译器会报错。
1 | public class ExceptionTest02 { |
处理异常案例3,针对编译时异常上抛和捕捉。
1 | public class ExcepTest03 { |
2.3 throws抛出异常
抛出异常指的是将异常抛给调用者,自身并不处理异常,相当于甩锅。对于运行时异常,可处理也可不处理;对于编译时异常,必须处理,无论是抛出还是捕捉,如果是捕捉,捕捉一次即可,如果是抛出,则需要一直抛出直到main方法或者直到捕捉。
案例1,采用一直上抛,代码如下所示:
1 | import java.io.FileInputStream; |
2.4 try…catch捕捉异常
捕捉异常指的是将异常捕捉并进行处理,不会让调用者知道,相当于对于调用者来说是“透明的”。最终JVM也就不会知道发生了异常,也就不会终止程序,保证了程序的健壮性。
案例2,采用捕捉,中间任何一步捕捉即可,代码如下所示:
1 | import java.io.FileInputStream; |
2.5 上抛和捕捉如何选择?
如果希望调用者来处理,那么就选择throws上抛,否则try…catch捕捉自己处理。
3. throws和throw
throw用于 创建完异常对象 手动抛出异常,可以认为throw是将异常抛给所在方法。
thows用于方法声明的时候抛出异常,属于异常处理的一种方法。
注意,仅仅创建完异常对象,此时JVM会认为是普通对象,并不会终止程序。
4. try…catch深入
catch括号中的异常类型可以是try语句块中的异常类型也可以是该类型的父类型(多态)。如果try语句块中的语句抛出多个异常,catch语句也可以捕捉多个异常,附加多个catch语句块即可。建议catch的时候,异常类型尽量小,精确地一个一个处理,这样有利于程序的调试。
1 | try{ |
注意,当有多个catch捕捉异常的时候,当出现了异常,就会自上而下匹配catch异常,所以当catch有多个的时候,自上而下设置的异常类型应该范围由小到大,否则,上面的大范围异常就会捕捉,而下面的小范围异常就会用不到,编译器报错。如下所示:
1 | public static void m3(){ |
**上面的FileNotFoundException类型异常的范围小于IOException,所以当fis.read()发生异常的时候,自上而下匹配catch异常,匹配到第二个IOException。而如果IOException写在FileNotFoundException上面的话,当FileInputStream发生异常的时候就会匹配到IOException,因为IOException的范围大,包括了FileNotFoundException,就会报错Exception 'java.io.FileNotFoundException' has already been caught
**。如下所示:
1 | public static void m3(){ |
另外,多个catch异常可以压缩成一个catch语句块,前提是几个catch异常不能有交集,如下所示:
1 | catch(FileNotFoundException | NullPointerException e) |
5. 异常对象的常用方法
异常对象有两个非常重要的方法:getMessage()和printStackTrace()。
getMessage()
获取异常简单的描述信息,即构造方法中的String参数。
printStackTrace()
打印异常追踪的堆栈信息,即平时运行程序中出现的红色异常信息。注意,异常堆栈信息是另一个线程负责输出,所以最终的输出顺序可能和代码的输出顺序不一致。一般情况下try…catch捕捉异常时的操作就是采用该方法打印异常堆栈信息。
6. finally关键字
其实在捕捉异常的时候,还有finally语句块,该语句块中的代码是最后执行的,并且是一定会执行的,即使出现了异常。注意,finally子句必须和try…catch语句块一起使用,不能单独编写。
finally语句通常使用在哪些情况下呢?通常在finally语句块中完成资源的释放/关闭,因为finally中的代码比较有保障。即使try语句块中的代码出现问题,finally语句块也会执行。
1 | public class ExceptionTest07 { |
另外,try可以和finally一起使用,但是try不能单独使用,**注意,只要有finally语句块,那么该语句块中的代码一定会执行,无论try中的语句是怎样的,除非try中有退出JVM的代码:System.exit(0);
**,如下所示:
1 | public class ExceptionTest08 { |
7. 补充
在Java中有一些强制性的规则(有些规则是不能破坏的,一旦这么说了,就必须这么做!):
- 方法体中的代码必须遵循自上而下顺序依次逐行执行(亘古不变的语法!)
- return语句一旦执行,整个方法必须结束,或者说return语句一定最后执行(亘古不变的语法!上面那个是退出JVM,和return无关)
下面的例子可能会违反前面所说的语句执行顺序,可以查看反编译后的代码。如下所示:
源码:
1 | public class ExceptionTest09 { |
反编译后的代码(可在IDEA对应项目下的out目录下查找,就是.class文件):
1 | public class ExceptionTest09 { |
可以看到,源码中的return语句返回的是finally操作之前的变量。
- return语句要最后执行,所以反编译(JVM内部实际代码)后将return语句放在了最后面。
- 要自上而下执行,return返回的是未finally操作后的代码,所以只能是保存临时变量,并新建变量供finally操作。
8. final、fianlly、finalize()
- final是一个关键字,表示最终的,不变的。
- finally也是一个关键字,和try联合使用在异常处理机制中,finally语句块中的代码一定会执行的。
- finalize()是Object类中的一个方法,作为方法名出现,用于垃圾回收回收时的一些操作。
9. 自定义异常
SUN提供的JDK内置的异常肯定是不够用的,在实际开发中,有很多业务出现异常之后,很多是JDK没有的,这时候就需要我们自定义异常。
查看一些异常类源码,可以发现,基本上只有无参构造方法以及有参构造方法两个方法,或者还有一些其他的私有方法以及属性等等。
9.1 自定义异常
有如下两个步骤:
- **编写一个类继承Exception(编译时异常)或者RuntimeException(运行时异常)**。
- 提供两个构造方法:无参构造方法,带有String参数的有参构造方法。
自定义异常如下所示:
1 | public class MyException01 extends Exception{ |
使用异常类如下所示:
1 | public class ExceptionTest10 { |
9.2 异常的作用
异常的作用就是为了当程序出现异常的时候,可以合理显著地提示程序员。而我们之前的操作基本上就是控制台打印一些提示信息(比如除数不能为0,栈已满等等)并return,这样做不是很合理,应该以异常的形式提示。
9.3 异常与方法重写
子类重写之后的方法不能比重写之前的方法抛出更多(更宽泛的异常),可以更小。注意:不抛异常 的 范围 比抛出异常 范围小(运行时异常除外)。
代码如下所示:
1 | class Animal{ |
一般情况下,父类的方法如何写,子类就如何写就行,不需要修改异常的抛出与范围。
10. 备注
参考B站《动力节点》。