异常的优点

现在你已经知道了什么是异常以及如何使用它们,现在是时候了解在程序中使用异常的优势了。

优点1:将错误处理代码与“常规”代码分开

异常提供了从程序的主逻辑中分离异常发生时应该做什么的细节的方法,在传统的编程中,错误检测、报告和处理通常会导致混乱的意大利面代码,例如,考虑这里的伪代码方法将整个文件读入内存。

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;
}

这里有很多错误检测、报告和返回,原始的七行代码在杂乱中丢失了,更糟糕的是,代码的逻辑流程也已丢失,因此很难判断代码是否正在做正确的事情:如果函数无法分配足够的内存,文件是否真的被关闭?在编写方法三个月后修改方法时,确保代码继续做正确的事情变得更加困难,许多程序员通过忽略它来解决这个问题 — 当程序崩溃时会报告错误。

异常使你能够编写代码的主流程,并在其他地方处理异常情况,如果readFile函数使用异常而不是传统的错误管理技术,那么它看起来更像是以下内容。

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;
    }
}

请注意,异常不会使你无需执行检测、报告和处理错误的工作,但它们确实可以帮助你更有效地组织工作。

优点2:在调用堆栈中传播错误

异常的第二个优点是能够在方法的调用堆栈中传播错误报告,假设readFile方法是主程序进行的一系列嵌套方法调用中的第四个方法:method1调用method2,它调用method3,最后调用readFile

method1 {
    call method2;
}

method2 {
    call method3;
}

method3 {
    call readFile;
}

假设method1是唯一对readFile中可能发生的错误感兴趣的方法,传统的错误通知技术强制method2method3readFile返回的错误代码传播到调用堆栈,直到错误代码最终到达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) {
    ...
}

方法可以通过在catch语句中指定任何异常的超类来基于其组或常规类型捕获异常,例如,要捕获所有I/O异常,无论其具体类型如何,异常处理程序都会指定IOException参数。

catch (IOException e) {
    ...
}

此处理程序将能够捕获所有I/O异常,包括FileNotFoundExceptionEOFException等,你可以通过查询传递给异常处理程序的参数来查找有关所发生情况的详细信息,例如,使用以下命令打印堆栈跟踪。

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

你甚至可以设置一个异常处理程序来处理任何Exception

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

Exception类接近Throwable类层次结构的顶部,因此,除了处理程序要捕获的那些异常之外,此处理程序还将捕获许多其他异常。如果你希望程序执行所有操作,你可能希望以这种方式处理异常,例如,为用户打印出错误消息然后退出。

但是,在大多数情况下,你希望异常处理程序尽可能具体,原因是,处理程序必须做的第一件事是确定发生了什么类型的异常,然后才能决定最佳的恢复策略。实际上,通过不捕获特定错误,处理程序必须适应任何可能性,过于通用的异常处理程序通过捕获和处理程序员没有预料到的异常,以及处理程序没有打算处理的异常,可以使代码更容易出错。

如上所述,你可以创建异常组并以一般方式处理异常,或者你可以使用特定的异常类型来区分异常并以精确的方式处理异常。

总结

程序可以使用异常来指示发生了错误,要抛出异常,请使用throw语句并为其提供异常对象 — Throwable的后代 — 以提供有关发生的特定错误的信息,抛出未捕获的已检查异常的方法必须在其声明中包含throws子句。

程序可以通过结合使用trycatchfinally块来捕获异常。

  • try块标识可能发生异常的代码块。
  • catch块标识一个代码块,称为异常处理程序,可以处理特定类型的异常。
  • finally块标识了一个保证执行的代码块,它是关闭文件、恢复资源以及在try块中包含代码之后进行清理的正确位置。

try语句应包含至少一个catch块或finally块,并且可能有多个catch块。

异常对象的类指示抛出的异常类型,异常对象可以包含有关错误的更多信息,包括错误消息,使用链式异常时,异常可以指向导致异常的异常,异常又可以指向导致它的异常,依此类推。


上一篇:如何抛出异常
下一篇:I/O流

博弈
2.5k 声望1.5k 粉丝

态度决定一切