计算机程序的思惟逻辑 (25) - 异常 (下)

本系列文章经补充和完善,已修订整理成书《Java编程的逻辑》(马俊昌著),由机械工业出版社华章分社出版,于2018年1月上市热销,读者好评如潮!各大网店和书店有售,欢迎购买:京东自营连接 html

上节咱们介绍了异常的基本概念和异常类,本节咱们进一步介绍对异常的处理,咱们先来看Java语言对异常处理的支持,而后探讨在实际中到底应该如何处理异常。java

异常处理

catch匹配

上节简单介绍了使用try/catch捕获异常,其中catch只有一条,其实,catch还能够有多条,每条对应一个异常类型,好比说:程序员

try{
    //可能触发异常的代码
}catch(NumberFormatException e){
    System.out.println("not valid number");
}catch(RuntimeException e){
    System.out.println("runtime exception "+e.getMessage());
}catch(Exception e){
    e.printStackTrace();
}
复制代码

异常处理机制将根据抛出的异常类型找第一个匹配的catch块,找到后,执行catch块内的代码,其余catch块就不执行了,若是没有找到,会继续到上层方法中查找。须要注意的是,抛出的异常类型是catch中声明异常的子类也算匹配,因此须要将最具体的子类放在前面,若是基类Exception放在前面,则其余更具体的catch代码将得不到执行。数据库

示例也演示了对异常信息的利用,e.getMessage()获取异常消息,e.printStackTrace()打印异常栈到标准错误输出流。经过这些信息有助于理解为何会出异常,这是解决编程错误的经常使用方法。示例是直接将信息输出到标准流上,实际系统中更经常使用的作法是输出到专门的日志中。编程

从新throw

在catch块内处理完后,能够从新抛出异常,异常能够是原来的,也能够是新建的,以下所示:数组

try{
    //可能触发异常的代码
}catch(NumberFormatException e){
    System.out.println("not valid number");
    throw new AppException("输入格式不正确", e);
}catch(Exception e){
    e.printStackTrace();
    throw e;
}
复制代码

对于Exception,在打印出异常栈后,就经过throw e从新抛出了。服务器

而对于NumberFormatException,咱们从新抛出了一个AppException,当前Exception做为cause传递给了AppException,这样就造成了一个异常链,捕获到AppException的代码能够经过getCause()获得NumberFormatException。微信

为何要从新抛出呢?由于当前代码不可以彻底处理该异常,须要调用者进一步处理。网络

为何要抛出一个新的异常呢?固然是当前异常不太合适,不合适多是信息不够,须要补充一些新信息,还多是过于细节,不便于调用者理解和使用,若是调用者对细节感兴趣,还能够继续经过getCause()获取到原始异常。运维

finally

异常机制中还有一个重要的部分,就是finally, catch后面能够跟finally语句,语法以下所示:

try{
    //可能抛出异常
}catch(Exception e){
    //捕获异常
}finally{
    //无论有无异常都执行
}
复制代码

finally内的代码无论有无异常发生,都会执行。具体来讲:

  • 若是没有异常发生,在try内的代码执行结束后执行。
  • 若是有异常发生且被catch捕获,在catch内的代码执行结束后执行
  • 若是有异常发生但没被捕获,则在异常被抛给上层以前执行。

因为finally的这个特色,它通常用于释放资源,如数据库链接、文件流等。

try/catch/finally语法中,catch不是必需的,也就是能够只有try/finally,表示不捕获异常,异常自动向上传递,但finally中的代码在异常发生后也执行。

finally语句有一个执行细节,若是在try或者catch语句内有return语句,则return语句在finally语句执行结束后才执行,但finally并不能改变返回值,咱们来看下代码:

public static int test(){
    int ret = 0;
    try{
        return ret;
    }finally{
        ret = 2;
    }
}
复制代码

这个函数的返回值是0,而不是2,实际执行过程是,在执行到try内的return ret;语句前,会先将返回值ret保存在一个临时变量中,而后才执行finally语句,最后try再返回那个临时变量,finally中对ret的修改不会被返回。

若是在finally中也有return语句呢?try和catch内的return会丢失,实际会返回finally中的返回值。finally中有return不只会覆盖try和catch内的返回值,还会掩盖try和catch内的异常,就像异常没有发生同样,好比说:

public static int test(){
    int ret = 0;
    try{
        int a = 5/0;
        return ret;
    }finally{
        return 2;
    }
}
复制代码

以上代码中,5/0会触发ArithmeticException,可是finally中有return语句,这个方法就会返回2,而再也不向上传递异常了。

finally中不只return语句会掩盖异常,若是finally中抛出了异常,则原异常就会被掩盖,看下面代码:

public static void test(){
    try{
        int a = 5/0;
    }finally{
        throw new RuntimeException("hello");
    }
}
复制代码

finally中抛出了RuntimeException,则原异常ArithmeticException就丢失了。

因此,通常而言,为避免混淆,应该避免在finally中使用return语句或者抛出异常,若是调用的其余代码可能抛出异常,则应该捕获异常并进行处理。

throws

异常机制中,还有一个和throw很像的关键字throws,用于声明一个方法可能抛出的异常,语法以下所示:

public void test() throws AppException, SQLException, NumberFormatException {
    //....
}
复制代码

throws跟在方法的括号后面,能够声明多个异常,以逗号分隔。这个声明的含义是说,我这个方法内可能抛出这些异常,我没有进行处理,至少没有处理完,调用者必须进行处理。这个声明没有说明,具体什么状况会抛出什么异常,做为一个良好的实践,应该将这些信息用注释的方式进行说明,这样调用者才能更好的处理异常。

对于RuntimeException(unchecked exception),是不要求使用throws进行声明的,但对于checked exception,则必须进行声明,换句话说,若是没有声明,则不能抛出。

对于checked exception,不能够抛出而不声明,但能够声明抛出但实际不抛出,不抛出声明它干吗?主要用于在父类方法中声明,父类方法内可能没有抛出,但子类重写方法后可能就抛出了,子类不能抛出父类方法中没有声明的checked exception,因此就将全部可能抛出的异常都写到父类上了。

若是一个方法内调用了另外一个声明抛出checked exception的方法,则必须处理这些checked exception,不过,处理的方式既能够是catch,也能够是继续使用throws,以下代码所示:

public void tester() throws AppException {
    try {
        test();
    }  catch (SQLException e) {
        e.printStackTrace();
    }
} 
复制代码

对于test抛出的SQLException,这里使用了catch,而对于AppException,则将其添加到了本身方法的throws语句中,表示当前方法也处理不了,仍是由上层处理吧。

Checked对比Unchecked Exception

以上,能够看出RuntimeException(unchecked exception)和checked exception的区别,checked exception必须出如今throws语句中,调用者必须处理,Java编译器会强制这一点,而RuntimeException则没有这个要求。

为何要有这个区分呢?咱们本身定义异常的时候应该使用checked仍是unchecked exception啊?对于这个问题,业界有各类各样的观点和争论,没有特别一致的结论。

一种广泛的说法是,RuntimeException(unchecked)表示编程的逻辑错误,编程时应该检查以免这些错误,好比说像空指针异常,若是真的出现了这些异常,程序退出也是正常的,程序员应该检查程序代码的bug而不是想办法处理这种异常。Checked exception表示程序自己没问题,但因为I/O、网络、数据库等其余不可预测的错误致使的异常,调用者应该进行适当处理。

但其实编程错误也是应该进行处理的,尤为是,Java被普遍应用于服务器程序中,不能由于一个逻辑错误就使程序退出。因此,目前一种更被认同的观点是,Java中的这个区分是没有太大意义的,能够统一使用RuntimeException即unchcked exception来代替。

这个观点的基本理由是,不管是checked仍是unchecked异常,不管是否出如今throws声明中,咱们都应该在合适的地方以适当的方式进行处理,而不是只为了知足编译器的要求,盲目处理异常,既然都要进行处理异常,checked exception的强制声明和处理就显得啰嗦,尤为是在调用层次比较深的状况下。

其实观点自己并不过重要,更重要的是一致性,一个项目中,应该对如何使用异常达成一致,按照约定使用便可。Java中已有的异常和类库也已经在哪里,咱们仍是要按照他们的要求进行使用。

如何使用异常

针对异常,咱们介绍了try/catch/finally, catch匹配、从新抛出、throws、checked/unchecked exception,那到底该如何使用异常呢?

异常应该且仅用于异常状况

这个含义是说,异常不能代替正常的条件判断。好比说,循环处理数组元素的时候,你应该先检查索引是否有效再进行处理,而不是等着抛出索引异常再结束循环。对于一个引用变量,若是正常状况下它的值也可能为null,那就应该先检查是否是null,不为null的状况下再进行调用。

另外一方面,真正出现异常的时候,应该抛出异常,而不是返回特殊值,好比说,咱们看String的substring方法,它返回一个子字符串,它的代码以下:

public String substring(int beginIndex) {
    if (beginIndex < 0) {
        throw new StringIndexOutOfBoundsException(beginIndex);
    }
    int subLen = value.length - beginIndex;
    if (subLen < 0) {
        throw new StringIndexOutOfBoundsException(subLen);
    }
    return (beginIndex == 0) ? this : new String(value, beginIndex, subLen);
}
复制代码

代码会检查beginIndex的有效性,若是无效,会抛出StringIndexOutOfBoundsException。纯技术上一种可能的替代方法是不抛异常而返回特殊值null,但beginIndex无效是异常状况,异常不能伪装当正常处理

异常处理的目标

异常大概能够分为三个来源:用户、程序员、第三方。用户是指用户的输入有问题,程序员是指编程错误,第三方泛指其余状况如I/O错误、网络、数据库、第三方服务等。每种异常都应该进行适当的处理。

处理的目标能够分为报告和恢复。恢复是指经过程序自动解决问题。报告的最终对象多是用户,即程序使用者,也多是系统运维人员或程序员。报告的目的也是为了恢复,但这个恢复常常须要人的参与。

对用户,若是用户输入不对,可能提示用户具体哪里输入不对,若是是编程错误,可能提示用户系统错误、建议联系客服,若是是第三方链接问题,可能提示用户稍后重试。

对系统运维人员或程序员,他们通常不关心用户输入错误,而关注编程错误或第三方错误,对于这些错误,须要报告尽可能完整的细节,包括异常链、异常栈等,以便尽快定位和解决问题。

对于用户输入或编程错误,通常都是难以经过程序自动解决的,第三方错误则可能能够,甚至不少时候,程序都不该该假定第三方是可靠的,应该有容错机制。好比说,某个第三方服务链接不上(好比发短信),可能的容错机制是,换另外一个提供一样功能的第三方试试,还多是,间隔一段时间进行重试,在屡次失败以后再报告错误。

异常处理的通常逻辑

若是本身知道怎么处理异常,就进行处理,若是能够经过程序自动解决,就自动解决,若是异常能够被本身解决,就不须要再向上报告。

若是本身不能彻底解决,就应该向上报告。若是本身有额外信息能够提供,有助于分析和解决问题,就应该提供,能够以原异常为cause从新抛出一个异常。

总有一层代码须要为异常负责,多是知道如何处理该异常的代码,多是面对用户的代码,也多是主程序。若是异常不能自动解决,对于用户,应该根据异常信息提供用户能理解和对用户有帮助的信息,对运维和程序员,则应该输出详细的异常链和异常栈到日志。

这个逻辑与在公司中处理问题的逻辑是相似的,每一个级别都有本身应该解决的问题,本身能处理的本身处理,不能处理的就应该报告上级,把下级告诉他的,和他本身知道的,一并告诉上级,最终,公司老板必需要为全部问题负责。每一个级别既不该该掩盖问题,也不该该逃避责任。

小结

上节和本节介绍了Java中的异常机制。在没有异常机制的状况下,惟一的退出机制是return,判断是否异常的方法就是返回值。

方法根据是否异常返回不一样的返回值,调用者根据不一样返回值进行判断,并进行相应处理。每一层方法都须要对调用的方法的每一个不一样返回值进行检查和处理,程序的正常逻辑和异常逻辑混杂在一块儿,代码每每难以阅读理解和维护。

另外,由于异常毕竟是少数状况,程序员常常偷懒,假定异常不会发生,而忽略对异常返回值的检查,下降了程序的可靠性。

在有了异常机制后,程序的正常逻辑与异常逻辑能够相分离,异常状况能够集中进行处理,异常还能够自动向上传递,再也不须要每层方法都进行处理,异常也再也不可能被自动忽略,从而,处理异常状况的代码能够大大减小,代码的可读性、可靠性、可维护性也均可以获得提升。

至此,关于Java语言自己的主要概念咱们就介绍的差很少了,接下来的几节中,咱们介绍Java中一些经常使用的类及其操做,从包装类开始。


未完待续,查看最新文章,敬请关注微信公众号“老马说编程”(扫描下方二维码),深刻浅出,老马和你一块儿探索Java编程及计算机技术的本质。用心原创,保留全部版权。

相关文章
相关标签/搜索