(25) 异常 (下) / 计算机程序的思维逻辑


(25) 异常 (下) / 计算机程序的思维逻辑
2016-07-08 老马 老马说编程 老马说编程
老马说编程

laoma_shuo

从入门到高级, 深入浅出, 老马和你一起探索编程及计算机技术的本质, 篇篇原创, 用心写作。

查看历史文章,请点击上方链接关注公众号。



上节我们介绍了异常的基本概念和异常类,本节我们进一步介绍对异常的处理,我们先来看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中一些常用的类及其操作,从包装类开始。


----------

更多好评原创文章

(1)  程序大概是怎么回事

(5)  小数计算为什么会出错?

(6)  如何从乱码中恢复 (上)?

(8)  char的真正含义

(12) 函数调用的基本原理

(17) 继承实现的基本原理

(18) 为什么说继承是把双刃剑?

(19) 接口的本质

(20) 为什么要有抽象类?

(21) 内部类的本质

(23) 枚举的本质

(24) 异常 (上)

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值