现在你知道了异常是什么,并且知道怎么使用它们,现在是时候讨论一下在你的程序中使用异常会有什么好处了。

优势1:隔离错误处理代码和常规代码

Exception提供了一种方法,把意外发生时的细节从程序主逻辑中隔离开来。在传统的编程中,错误的检测、报告和处理通常会导致像意大利面条那么混乱的代码。例如,请看下面的伪码,它把整个文件读入内存:

readFile {
    open the file;
    determine its size;
    allocate that much memory;
    read the file into memory;
    close the file;
}

第一眼看过去,这函数简单到不能再简单,但是它忽略了下列所有潜在的问题:

为了处理这些问题,readFile函数必须有更多的代码来处理错误检测、报告和处理。这个函数可能会变成这样:

errorCodeType readFile {
    initialize errorCode = 0;
   
    open the file;
    if (theFileIsOpen) {
        determine the length of the file;
        if (gotTheFileLength) {
            allocate that much memory;
            if (gotEnoughMemory) {
                read the file into memory;
                if (readFailed) {
                    errorCode = -1;
                }
            } else {
                errorCode = -2;
            }
        } else {
            errorCode = -3;
        }
        close the file;
        if (theFileDidntClose && errorCode == 0) {
            errorCode = -4;
        } else {
            errorCode = errorCode and -4;
        }
    } else {
        errorCode = -5;
    }
    return errorCode;
}

有那么多的错误检测、报告和返回语句,让原本简单的5行代码完全迷失在乱哄哄的代码里面了。更糟糕的是,代码的逻辑流程也随着丢失了,因此很难说这段代码是否做了正确的事:当函数分配内存的时候失败了,文件能否确保被关闭?当你三个月之后再修改这段代码时,你就更难确定改完后代码还到底能不能用了。很多程序员解决的办法就是简单地忽略它——当他们的程序崩溃了报告一下错误就行。

Exception可以让你能在一个地方写好代码主流程,在另外一个处理异常情况。如果readFile函数改用Exception来替换传统的错误管理技术,看起来就像下面的代码:

readFile {
    try {
        open the file;
        determine its size;
        allocate that much memory;
        read the file into memory;
        close the file;
    } catch (fileOpenFailed) {
      doSomething;
    } catch (sizeDeterminationFailed) {
        doSomething;
    } catch (memoryAllocationFailed) {
        doSomething;
    } catch (readFailed) {
        doSomething;
    } catch (fileCloseFailed) {
        doSomething;
    }
}


要注意的是,使用Exception并不会让你在检测、报告和处理错误时更省力,但是可以让你把这些工作组织起来更高效。

优势2:在调用栈中向上传播错误

Exception的第二个优势就是,可以把错误报告给调用栈的上层方法。假如在主程序中,在一系列嵌套的调用方法中,readFile方法是第四个方法: method1调用method2,然后调用method3,最后调用readFile。

method1 {
    call method2;
}

method2 {
    call method3;
}

method3 {
    call readFile;
}

假设method1是唯一关心在readFile内发生了什么错误的方法。传统的错误通知技术强制method1和method2传播readFile返回的错误代码,直到错误代码最后传到method1——而method1是唯一需要返回码的方法。

method1 {
    errorCodeType error;
    error = call method2;
    if (error)
        doErrorProcessing;
    else
        proceed;}

errorCodeType method2 {
    errorCodeType error;
    error = call method3;
    if (error)
        return error;
    else
        proceed;}

errorCodeType method3 {
    errorCodeType error;
    error = call readFile;
    if (error)
        return error;
    else
        proceed;}

回忆一下,Java运行时系统通过逆向搜索,查找对某个异常感兴趣的任何方法。一个方法可以躲避任何抛给它的异常,从而让一个方法可以把异常抛到更远的调用栈中捕获。因此,只有关心错误的方法,才必须要去检测错误。

method1 {
    try {
        call method2;
    } catch (exception e) {
        doErrorProcessing;
    }
}

method2 throws exception {
    call method3;
}

method3 throws exception {
    call readFile;
}

然而,正如伪代码所示,中间人方法可以躲避掉后面必须要处理的异常。一个方法通过在throws从句中进行声明,可以抛掉任何检查异常。

优势3:归类和区分错误类型

因为程序抛出的所有异常都是对象,类是有层次结构的,所以对异常进行归类或分组是非常自然的结果。在Java平台中有一个例子,一组相关异常被定义在java.io包——IOException及其子类。IOException是最通用的,它代表了我们在执行I/O相关操作的时候发生的任何错误类型。它的继承者代表了更加细分的错误。例如,FileNotFoundException表示一个文件不能再磁盘中定位到。

一个方法可以写一个具体的处理器,能处理一个十分具体的异常。由于FileNotFoundException没有继承者,所以下面的这个异常处理器只能处理一种类型的异常:

catch (FileNotFoundException e) {
    ...
}

一个方法也可以用更通用的处理器捕获处理具体的异常。例如,为了捕获所有的I/O异常,不管具体的类型是什么,只要给异常处理器指定一个IOException参数就行。

catch (IOException e) {
    ...
}

这个处理器可以捕获所有的I/O异常,包括FileNotFoundException,EOFException等等。你可以通过查询传给异常处理器的参数,发现错误发生的细节。例如,用下面的代码打印堆栈跟踪信息:

catch (IOException e) {
    // Output goes to System.err.
    e.printStackTrace();
    // Send trace to stdout.
    e.printStackTrace(System.out);
}

你甚至可以构建一个可以处理所有异常的异常处理器:

// A (too) general exception handler
catch (Exception e) {
    ...
}

在Throwable类层级结构中,Exception类已经快接近顶点了。所以这个处理器将会捕获很多它不想捕获的异常。如果你想在整个程序中对所有异常都采取统一处理方式,你就可能会采取这种办法处理异常,例如,给用户打印错误信息并退出。

然而在大多数情况下,你希望异常处理器越具体越好。理由是在你决定最佳的恢复策略之前,你首先要知道错误的类型。事实上,如果不捕获具体的错误,这个处理器就必须要容纳任何可能性。太通用的异常处理器可能会让代码更容易出错,因为它们会捕获和处理程序员意料之外的异常,这样就超出处理器的能力范围了。

就像前面说过的,你可以创建一组异常,然后采用通用处理的风格。或者你可以使用具体的异常类型来区分不同的异常,然后用精确的风格处理异常。





原文地址