6.6 异常传递

    包括 Java 语言在内的很多现代语言的异常处理机制中,异常可传递至调用方。假设函数 f 调用函数 g,后者又调用函数 h。如果函数 h 中有异常抛出且在函数 h 中无法处理该异常,那么就会看函数 g 能否处理该异常。如果函数 g 也无法处理,接下来就看函数 f 可不可以。如果没有哪个函数可以处理该异常,程序就会异常终止。

    异常传递的问题

    大家或许认为这样做是理所当然的。其实关于这个意见并未统一,因为这一设计有一个很大的问题。那就是,即使看到了函数 f 的代码也不知道函数 f 可能会抛出什么异常。有可能是函数 f 调用的另外的函数 g 中抛出的异常传递过来的,也有可能是函数 g 调用的函数 h 抛出的异常。也就是说,如果不看见函数 f 调用的所有的函数代码,就无从得知函数 f 抛出何种异常。万一没有察觉到抛出某种异常的可能性,程序就有可能异常终止。

    Java 语言的检查型异常

    在 6.3.1 节提到的论文中,Goodenough 主张为了避免这一问题,需要明确地声明可能抛出的异常。Java 语言就采用了这一方针,我们现在就来看一下。

    其他语言中所谓的异常,Java 语言中的 throw 语句也能抛出,并进一步分为三类:不应该做异常处理的重大问题、可做异常处理的运行时异常和可做异常处理的其他异常。这里的其他异常叫做检查型异常,如果在方法之外抛出,就需要在定义方法时声明。

    throws 就是为这个目的准备的。以下代码中的 void shippai ( ) throws MyException,实质上是声明了这个方法有可能抛出 MyException 的异常。

    Java
    class Foo {
    // shippai抛出MyException异常
    void shippai() throws MyException{
    throw new MyException();
    }

    // 使用shippai(方法1)声明'throws MyException'、
    void foo() throws MyException{
    shippai();
    }
    // (方法2)用catch捕捉MyException异常、错误处理
    void bar(){
    try{
    shippai();
    }catch(MyException e){

    }
    }
    }

    class MyException extends Exception {}

    使用了检查型异常,就不会发生漏查抛出异常的可能性。当一个方法调用可能抛出异常的方法 shippai 时,可以选择两种方法来实现异常处理,将 shippai 抛出的异常传递至调用处,或者让 shippai 自己处理该异常。前者就如方法 foo 那样用 throws 声明,后者就如方法 bar 那样用 try/catch 括起来的语句实现异常处理。如果没有采用两者中任何一种方法,编译器就会指示出遗漏错误。

    检查型异常没有得到普及的原因

    可以说检查型异常是一种非常好的机制。但是这种机制并没有很好地普及到其他语言中,这是为什么呢?

    一言以蔽之,就是因为它太麻烦。一旦 throws 或 try/catch 中异常的数目增多,或者某一方法需要追加一种异常,就不得不修改调用了该方法的所有方法,特别麻烦。

    C# 语言虽然很大程度上参考了 Java 语言,但也没有采用检查型异常机制。究其原因,C# 语言的设计者 Anders Hejlsberg 如是说:“检查型异常的理念本身没有什么不对的地方,相反是很棒的。然而在像 Java 语言这样的实现方式下,它在解决某些问题的同时又引入了别的问题。如果能有更好的实现方法,C# 语言或许也是可以借用的 22。”

    22请参考“The Trouble with Checked Exceptions”,http://www.artima.com/intv/handcuffs.html
    专栏
    具体的知识和抽象的知识
    在语言 X 中如何实现 Y,像这种具体的知识(know-how)可快速提高你的工作效率。但是一旦语言发生变化,这种知识就无法再使用。世界瞬息万变,这意味着限定了应用范围的具体知识将慢慢失去其价值。因此,我们不仅要学习具体的知识,更要有意识地去学习那些应用范围广泛的抽象的概念。
    当然,学习了抽象的元知识,如果不将其与你具体的经验相结合,也无法在实际应用中发挥其作用。喜欢樱花的人即使剪下花开的树枝带回家,终将看到的也仅仅是枝枯花败的场景而已。要想樱花年年盛开,离开根部和枝干是不行的。
    我们所学的知识到底有没有真正的“根基”,可以通过考察能否具体地举例或者具体地实现来确认。没有真正根基的知识是无法顺藤摸瓜、触类旁通的,所谓学习到的知识也只能像鹦鹉学舌般的重复讲讲而已。想要因地制宜地活用知识更是缘木求鱼,根本没有可能了。

     

    专栏
    学习讲求细嚼慢咽
    一口吞不下一整块肉。首先要把肉切成能入口的大小,嚼碎后再吃。同样的道理,对抽象的概念、复杂的系统和不习惯的领域,我们也不可能一下子理解通透。首先要把信息切分,一小块一小块地消化吸收到自己的大脑里。
    然而,在信息爆炸时代,如何对信息做取舍呢?什么信息是重要的,什么是不重要的?要判断什么信息重要首先需要对其有深刻的理解,但如此一来,就陷入到先有蛋还是先有鸡的困境中了。
    身边如果有这熟悉这些信息的人、朋友,向他们请教也是一种方法。但要是没有呢?在网上检索查询的话,那些发言者是真的熟悉还是装作熟悉呢,这又该如何判断?
    作者本人写的文档当然是最为翔实的。但是要么认为难懂,要么认为内容太多,要么认为看英语有困难,我们总会有各种借口放弃查阅原作者的一手资料,而去寻找那些写得简单的解说。这就如同,当肉太大、太硬时,转而不顾食品安全,吃起别人做出来的肉馅。
    这种心情是可以理解的。笔者也有在庞大信息量面前心力交瘁的时候。这时有三种战略可供参考:从需要的地方开始阅读,先掌握概要再阅读细节,从头开始逐章手抄。关于它们的详细介绍将在接下来的章节介绍。