深入了解Java中的异常

时间:2022-03-31 17:36:17

虽然在编译时发现错误是最理想的情况,但是这种情况并不是很容易产生,大多数的错误是在运行期间发生的,Java中的异常就是为了在运行时能够检查程序的错误。通过异常,我们能够简化错误代码处理的逻辑,如果不使用异常,我们就要检查特定的错误,这样就会出现很多的if...else...语句,如果使用了异常机制则可以不必在方法实现中进行检查,而是通过异常机制进行捕获,这样可以将方法实现的具体逻辑和遇到问题时的解决办法解耦,而且如果遇到的异常能够处理则进行捕获处理,如果不能处理可以抛到上层调用API中,交由上层处理。

异常抛出会导致原来程序执行的顺序将会被终止,因此异常亦可以控制程序流程,当异常抛出后程序执行流程如下:

  • 程序使用new在堆上创建异常对象;
  • 当前执行路径被终止,从当前环境获取异常对象的引用;
  • 异常处理程序接管业务程序,以便将程序从错误状态中恢复以使程序继续运行或者通过另外的方式运行;

1. 异常的捕获

所谓的异常捕获,即对可能出现异常的代码进行监控,如果处于监控区域的代码发生异常则执行对应的异常处理程序,java中的语法支持即为try...catch...

try{
// 监控区域
}catch(Exception type1 e1){
// 异常处理程序1
}catch(Exception type2 e2){
// 异常处理程序2
}

具体的执行流程为:

  • 当try监控区域的代码发生异常并抛出时,会首先在内存的堆上实例化该异常;
  • 将该异常对象顺序的传递到每个catch语句进行匹配,对应这种异常类型的catch语句中执行对应的异常处理程序,此处要注意只会执行一个catch语句,之后的catch语句不会再执行;

有捕获异常时在所有的catch语句块中顺序执行的机制,如果想捕获所有的异常则可以在最后加上所有异常的基类Exception来匹配所有的异常,但是对应该异常的catch语句一定要放到最后,以防它在其他异常之前被捕获!

如上的try…catch…语句是最基本的,另外还发展出进化的异常捕获语句:

// 用于资源自动关闭
try(){
// 监控区域
}catch(Exception type1 e1){
// 异常处理程序
}

// 用于在一个catch中捕获两种类型的异常
try{
// 监控区域
}catch(Exception type1 | Exception type2 e){
// 异常处理程序
}

对于捕获的异常一般有两种方法进行处理:

  • 终止程序的运行:一旦异常被抛出,由于无法采取有效的措施来弥补,所以只能终止程序的运行;

  • 恢复程序的运行:通过一些措施来恢复程序的正常运行,比如将try语句放到while(true)循环中执行,通过在catch语句中修复程序达到程序正常运行,如下:

    while(true){
    try{
    // 可能发生异常的代码
    break;
    }catch(Exception e){
    // 异常处理程序,恢复了程序的运行
    }
    }

    比如在发生FileNotFoundException时,可以通过在catch语句中所有文件的上层和同级目录来完成查看文件是否存在,以此来恢复程序的正常执行。

但是从实际开发角度来说,终止程序的运行可能会更常用,因为有时候我们并不能清楚的定位异常发生的原因,或者知道原因但是发现无法解决因此只能抛出来终止程序的运行。

2. 异常体系分析

Java 中的异常继承体系如下:

深入了解Java中的异常
深入了解Java中的异常

上图只是Java提供的异常中的冰山一角,但是从继承体系来说,所有的异常都是直接或者间接来自于Throwable这个类,

但是从图上可以看出Throwable的直接子类有ErrorException,那么它们之间有什么区别呢?

  • 首先是Error:这个类应该在系统内部使用,用来描述在编译期间的系统级或者更底层的错误,一般来说我们不应该自定义这个类的子类,也不应试图去捕获它,常见的Error的子类,比如NoClassDefFoundError
  • 之后是最常见的也是我们使用最多的Exception,Exception又分为两种
    • 一般的异常,也称受检异常:该类异常是在编译期间发现的,一般来说这种异常是强制提醒开发者来进行处理以使程序恢复运行,而且也是可以通过一些措施来弥补的;
    • 运行时异常,也称非受检异常:这种异常是在程序运行期间抛出的,而且一般来说这种异常也无法由开发者处理,所有这种异常都会由虚拟机抛出,所以也就不必在异常说明语句throws后列举,最常见的就是臭名昭著的NullPointerException

值得注意的问题是,除了RuntimeException异常,任何其他的异常都不应该忽略,因为对于RuntimeException类型的异常通常都是因为一些无法预料的问题,这些问题在开发者的预料之外,但是我们应该尽可能对一些代码进行检查和校验以避免这种运行时异常,但是除了这种异常,其他的异常都应该被重视和处理。

3. 创建自定义异常

虽然Java提供给我们很多的异常类以供使用,但是出于业务的原因,我们可能更倾向于定义自己的符合业务需求的异常类,但是通常来说会有几种原则遵循(自己的体会):

  1. 首先确定自定义的异常是能恢复的还是不能 的,这决定你定义的是受检异常还是RuntimeException类型的异常,这要根据你想要采取的处理措施,如果你觉得发生这种情况根本无法处理,那就果断选择RuntimeException异常;
  2. 自定义的异常不要直接继承自Throwable或者Exception,而是继承自意义相近的异常,比如当传入的参数不对时,可以直接继承IllegalArgumentException
  3. 由于异常类的本职工作就是用来处理错误情况的,所以其中不用添加过多的逻辑,因为可能基本上都用不上,只需要添加相应的构造器和异常情况说明即可,而且构造器可以直接调用父类的构造器完成初始化;

下面通过一个具体的例子说明自定义异常的使用:

实际情况为:我们假设程序员发工资的时候需要输入一个数值,这个数值当然不可能是负数(加班那么长时间,看文档比老婆次数还多,还给发负数!!),所以我们需要定义一个异常,当输入的参数是负数时抛出这个异常。

根据上面说的原则进行对照:

  1. 输入负数的时候我们有什么办法吗?当然没有,我们自己又不能把这个数变成正数(如果可以的话,我们还天天傻傻的对着电脑干嘛!),所以是RuntimeException异常;
  2. Java提供的异常中有没有对应的意义相近的异常,经过查找有就是我们上面说的IllegalArgumentException,那么我们的异常就直接继承该异常即可;
  3. 这个异常会完成什么功能,当然就是抛出这个错误,其余什么也不用干,也干不了!

OK,分析完可以开始定义异常如下:

public class ArgumentNegativeException extends IllegalArgumentException {
public ArgumentNegativeException(){}
public ArgumentNegativeException(String message){
super(message);
}
public ArgumentNegativeException(String message, Throwable cause){
super(message, cause);
}
}

可以看出上面的异常类很简单,只是实现了三个父类的构造器,一般来说在自定义异常中实现这三个异常已经完全足够,或者说也可以减少一些用不到的,下面定义我们的发工资的方法:

public static void testDispatch(int count) {
if(count < 0){
throw new ArgumentNegativeException("f**k, the salary is negative");
}
// 全部上交
// 老婆发给这个月的生活费
// ......
}

从上边可以看出由于我们自定义的异常是继承自RuntimeException,所以不需要显式使用throws抛出,而是在遇到异常时将该异常交给JVM来抛出!使用testDispatch(-20000)测试如下:

深入了解Java中的异常
深入了解Java中的异常

顺便说一句,RuntimeException异常是能捕获的,但是平常从网络看到的从没有不捕获过,这是因为这种异常设计的就是为了无法处理的情况,所以不建议捕获,但是不建议捕获不是不能捕获,如下:

public void testException() {
try{
System.out.println("try statement");
throw new ArgumentNegativeException("ArgumentNegativeException is thrown");
}catch(ArgumentNegativeException e){
System.out.println("catch statement: " + e.getLocalizedMessage());
}
}

执行的结果如下:

深入了解Java中的异常

在使用异常的过程中,免不了要获取抛出异常时的相关信息,对于这一点,Java的设计者已经帮我们想到了,提供了许多有用的API,而这些API多数是由Throwable类提供的,如下:

  • getMessage():获取异常的详细信息;

  • getLocalizedMessage():在Java内部实现实际上调用的是getMessage(),所以文档建议我们覆盖官方的实现;

  • toString():实际上该方法是对getLocalizedMessage()的包装实现,如下:

    public String toString() {
    String s = getClass().getName();
    String message = getLocalizedMessage();
    return (message != null) ? (s + ": " + message) : s;
    }
  • printStackTrace(PrintStream s)等一系列的方法族:该系列API是将异常的堆栈信息打印到打印流中,该信息会显示异常抛出的源头在哪里,可能也是我们最常见的异常方法,但是不建议直接使用该方法,而是在捕捉异常时将该异常信息转化为字符串作为一个消息返回给调用者,如下:

    public static String stackTrace2String(Exception ex) {
    StringWriter writer = new StringWriter();
    ex.printStackTrace(new PrintWriter(writer));
    return writer.toString();
    }
  • getStackTrace():该方法可以直接获取printStackTrace()打印的信息,该方法返回一个由堆栈信息组成的数组,每个元素表示方法调用序列中的一次调用,元素0为表示最后一个方法调用,也就是异常生成和抛出的地方,最后一个元素是第一个方法调用,getStackTraceDepth()能够返回调用深度;

基本上上面所说的几个API已经能够满足我们的需要了。

4. 异常链

什么是异常链?我们常常想在捕获一个异常之后再抛出另外一个异常,并且想要保存之前异常的信息,从而将所有异常信息联系到一起,这个处理过程称为异常链。这种处理场景一般发生在自定义异常时较多,比如想把系统抛出的异常转译为与业务相关的自定义异常。

那么怎么获取异常之前的信息呢?有两种方法可以使用:

一种是通过构造器,通过public Throwable(String message, Throwable cause){...}或者public Throwable(Throwable cause){...},然后通过getCause()方法将原始异常的原因获取放到自定义异常中,以上面的自定义异常为例如下:

try{
//...
}catch(NullPointerException e){
// 将NullPointerException异常信息放到新异常中
throw new ArgumentNegativeException("", e.getCause());
}

另一种是通过initCause()尤其是在异常的构造器中没有Throwable参数时,使用该方法则是唯一选择,如下:

try{
//...
}catch(NullPointerException e){
// 将NullPointerException异常信息放到新异常中
ArgumentNegativeException e1 = new ArgumentNegativeException("");
e1.initCause(e);
throw e1;
}

5. 蛋疼的Finally

Finally语句则是无论是否有异常抛出都会被执行,它的作用一般来说是进行资源释放以及一些其他的后续的处理工作,但是涉及到具体的执行会有很多让人迷惑的地方,比如和return语句谁先执行,关于 Java 中 finally 语句块的深度辨析这篇文章说的很详细也很深入,感觉自己不会比他分析的更好。