怎样用好异常机制

怎样用好Java异常机制

本文档用于2020/4/1讨论课

BUAA.1823.邓新宇 2020/3/30


认识Java的异常机制

Throwable接口&Exception类

Java的异常信息以Throwable接口的实例作为载体,它们和别的类一样,也需要在heap上分配空间。
这个类的对象主要有三个属性:String message、Throwable cause、StackTraceElement[] trackback。分别代表着文字描述信息、内层的异常、异常发生的调用路径,其中只有最后一个属性有set访问器,别的属性都只有get访问器,只能通过构造器初始化一个值。

异常传递机制

每个线程都有一个栈,随着函数的调用,栈在一帧一帧增长,当异常抛出时,会一帧一帧弹出,直到遇到可以catch住该异常的地方。

父线程不可以catch子线程的异常


怎么使用异常机制

Java的异常在使用的时候有哪些部分?

  1. throw----抛出异常
  2. throws---声明异常
  3. catch----处理异常

什么时候应该做什么呢?

抛出异常

当出现阻碍程序进行运行,并且本地无法处理的问题时就需要抛出异常

声明异常

当我们定义一个可能会抛出异常的方法时,应当在函数声明的位置用throws关键字声明它可能抛出的异常,Java会强制要求会抛出异常的方法使用throws关键字,也会强制调用这些方法的时候必须做好异常处理。因为这种强制性,当继承一个类/实现一个接口的时候,Override方法不能抛出父类未声明的异常。

两个特殊情况:

  • 抛出RuntimeException及其所有子异常的方法都不必要声明throws;
  • 可能被继承的类/接口/抽象类,声明抛出一个异常,但实际上不抛出,这是为了其子类可以抛出该异常。

Tips:如果必须要抛出一个父类没有声明的异常,可以将这个异常嵌套在一个RuntimeException内抛出。【但这很危险,不建议这样做】

捕获与处理异常

try{} catch(CertianExceprion e){} finally{}

一个try可以配合多个catch,捕获异常时从第一个异常开始尝试匹配,同名异常和其子类异常会被匹配,也就是说如果同时需要catch异常FatherException和其子类异常ChildException extend FatherException,并进行不同的处理,就需要把子类写在前面。当被抛出的异常与一个catch匹配了,就不会再试图匹配其他的异常了。

finally是这个结构中无论是否有异常,都会执行。即使在前面发生了return/throw,这一部分也会被执行。


怎样用好异常?

了解RuntimeException意味着什么

官方文档这样描述RuntimeException:

RuntimeException is the superclass of those exceptions that can be thrown during the normal operation of the Java Virtual Machine.
RuntimeException and its subclasses are unchecked exceptions. Unchecked exceptions do not need to be declared in a method or constructor's throws clause if they can be thrown by the execution of the method or constructor and propagate outside the method or constructor boundary.

RuntimeException是平常用的操作抛出的异常,而且不要求throws标识,这意味着这类异常往往不要求特殊地catch它们(做个除法就要try-catch一下ArithmeticException,不觉得很难受?)一般情况下是预设不会抛出这样的错误的。

因为RuntimeException及其子类不需要throws标识,所以也叫unchecked异常,对应地,其它异常都叫checked异常。

这类异常在大多数情况下是编程错误导致的,不是程序员可以处理的,一般不要catch,就直接让程序crash掉,然后开始改bug就好了。

抛出正确的异常

抛出异常有两个问题:

  1. 需不需要是RuntimeException?
  2. 有没有现成的Exception?

当一个异常是外部也没法被处理的,才抛出RuntimeException,否则尽量抛出一个checked异常。

抛出异常的时候优先选择现成的异常,而不是自定义新的异常。Java的官方文档已经列出很多Exception了,如果这些异常的含义和自己想要抛出的异常一样,就抛出原有的异常即可,这样在处理的时候就可以一起处理了,也避免调用者看到一个完全不认识的异常感到迷茫。
想要抛出的异常,如果具有通用性,并且可以找到与其意义相同的已有异常,是可以直接将其抛出的。但是业务逻辑相关的异常尽量要自定义,自定义的时候可以继承自已有的、意义相近的异常。因为异常的catch机制注定了异常对象是一种极度依赖于类名对象,只有自定义异常才能保证足够的区分度。

只有可以处理的异常才catch

完全不能处理的异常不要catch。完全不能处理的异常catch下来,外层就不知道内层发生了错误,本来可以处理这种异常的地方就获得不了异常信息了。

但可以先catch再重新抛出:

  1. 为内层捕获到的异常添加一些信息/进行一些处理;
  2. 做一些善后工作;
private BlockingQueue<Passenger> container = LinkedBlokingQueue<>(10);
public void loadPassenger(Passenger passenger) throws ElevatorIsFullException {
    try {
        container.add(passenger);
    }
    catch (IllegalStateException e) {
        door.close();
        throw new ElevatorIsFullException(this.toString() + " has already full!", e);
    }
}

终止or恢复

有些异常意味着无法继续执行,这种异常的发生意味着一部分代码被跳过,并用妥协方式进行代替。

而有些异常却可以解决问题并尝试恢复运行。这种情况下,一般需要这么写:

boolean flag = true;
while (flag) {
    try {
        //do something that may throw an Exception
        flag = false;
    }
    catch (CertainException e) {
        //change some status to make this run again
    }
}

这样的代码使得程序可以克服一些问题继续运行。但是这样将导致一定的耦合度增加,因为外部处理恢复的程序必须知道到底为什么抛出这样的异常,才能解决并恢复运行。


危险行为

抛出父类没有声明的异常。Override一个方法的时候,如果要抛出一个父类没有throws声明的checked异常CheckedException,只能通过 throw new RuntimeException(new CheckedException())来将其抛出。这个时候你的外层代码可能需要这么写:

try {
    //do something
}
catch (RuntimeException e) {
    if (e.getCause() instanceof CheckedException) {
        //do something to solve the Exception
    }
    else {
        throw e;
    }
}

或者自定义一个RuntimeException。但是这种异常抛出是没有强制的try-catch检查的,别人在调用的时候大概率会忽略掉这个异常的处理。最最重要的是,这样将导致调用者必须了解被调用的部分是如何运作的,才能写出正确的异常处理,违反了封装的初衷。

finally中抛出异常。Java有一种情况可能导致旧的Exception未被处理就抛出新的异常:当try部分可能抛出的某些Exception未被catch处理,而finally中抛出了新的异常,此时内部的那个异常就会丢失。

构造器中使用会抛出异常的方法。构造器承担着对象初始化的任务,所以在构造器中处理异常的时候一定要清楚哪一部分在处理异常的时候已经被正确初始化了,哪一部分未被初始化。如果该错误甚至会导致新的对象创建失败,那么就可以向外抛出异常。

抑制异常。就是catch住异常不做任何有效处理,这将导致本可以处理的异常无法被处理,也会导致发生了异常而外部不知道,给debug带来困难。这包括两种方式:1、空的catch;2、利用Exception控制程序运行流程。第一种好理解,第二种,举个例子,Scanner就通过构造时的InputStream抛出的IOException来判断是否扫面到结尾,这将导致因为其它原因造成的IOException被忽略。(PS:也不要用throws-catch来跳出多层循环,应当使用的是break + 标签,类似于goto)

catch (Exception e) {xxxxx}。直接catch这种顶层的异常(或者catch (RuntimeException e) {xxxx}),但是异常处理程序却只是针对某个异常的,将导致错误地处理了其它的异常,也可能将导致本来应该致命的错误未能让程序停下来。所以一般这些catch中都需要抛出新异常/直接把原来的异常对象抛出去,或者记录在日志中。


多线程的异常

InterruptedException

表示当前线程被中断了。接收到这个信号后应当开始着手进入终结程序了。

Eg:电梯接收到InterruptedException之后,拒绝任何乘客上电梯,把当前电梯内的乘客送到目的地就结束run方法。

但是JVM只会在sleep、wait等方法调用导致的阻塞的时候被中断才会抛出这个异常,其它情况下要主动调用Thread.interrupted()来查看状况。官方文档给出了一种比较常见的写法:

if (Thread.interrupted()) {
    throw new InterruptedException();
}

InterredException提供了一种比较从容的线程中断机制,而不是提供了一种线程的唤醒机制,唤醒应当考虑的是notify()。

interface Thread.UncaughtExceptionHandler

接口要求的方法:

void uncaughtException(Thread t, Throwable e)

Thread类两个使用该接口的方法:

void setUncaughtExceptionHandler(Thread.UncaughtExceptionHandler eh)
void static setDefaultUncaughtExceptionHandler(Thread.UncaughtExceptionHandler eh)

第一个方法:为指定线程设置一个Handler
第二个方法:为所有线程设置一个默认的Handler

当前进程有一个没被catch住的Exception时,会调用这个uncaughtException方法。


最后

  • 危险行为在某些时候是迫不得已的选择,但是在进行这些危险行为的时候一定要意识到现在这段代码很危险。
  • 确定出现了本地无法处理的问题才抛出异常,确定某个异常可以处理才catch。
  • 异常提供了一种把本地无法解决的问题向外传递、寻找能够解决这个问题的地方的机制,尽量不要用它做一些别的骚操作。

参考资料:

  1. Java编程思想(Thinking in Java) by Bruce Eckel 第十二章-通过异常处理错误
  2. Java官方文档
posted @ 2020-04-01 20:27  SnowPhoenix  阅读(178)  评论(0编辑  收藏  举报