当前位置:   article > 正文

Java Unchecked Exceptions — The Controversy

Java Unchecked Exceptions — The Controversy

由于Java编程语言不需要方法来捕获或指定未检查的异常(RuntimeException、Error及其子类),因此程序员可能会试图编写只抛出未检查异常的代码,或者使其所有异常子类都继承自RuntimeException。这两种快捷方式都允许程序员编写代码,而不必费心处理编译器错误,也不必费力指定或捕获任何异常。尽管这对程序员来说似乎很方便,但它回避了捕获或指定需求的意图,并可能会给使用您的类的其他人带来问题。
为什么设计人员决定强制方法指定可以在其范围内引发的所有未捕获的检查异常?方法可以引发的任何异常都是该方法的公共编程接口的一部分。调用方法的人必须知道方法可以抛出的异常,以便他们可以决定如何处理它们。这些异常与其参数和返回值一样,也是该方法编程接口的一部分。
下一个问题可能是:“如果记录方法的API(包括它可以引发的异常)非常好,为什么不也指定运行时异常?”运行时异常表示编程问题导致的问题,因此,不能合理地期望API客户端代码从中恢复或以任何方式处理它们。这类问题包括算术异常,例如被零除;指针异常,例如试图通过空引用访问对象;以及索引异常,例如试图通过太大或太小的索引访问数组元素。

运行时异常可以发生在程序中的任何位置,在典型的程序中,它们可能非常多。必须在每个方法声明中添加运行时异常会降低程序的清晰度。因此,编译器不需要捕获或指定运行时异常(尽管可以)。
抛出RuntimeException是常见做法的一种情况是用户错误地调用方法。例如,方法可以检查其参数之一是否不正确地为null。如果参数为null,该方法可能会引发NullPointerException,这是一个未检查的异常。
一般来说,不要仅仅因为您不想为指定方法可以引发的异常而烦恼,就抛出RuntimeException或创建RuntimeException的子类。
下面是底线准则:如果可以合理地期望客户端从异常中恢复,则将其设置为检查异常。如果客户端无法执行任何操作来从异常中恢复,请将其设置为未检查的异常。

一、Exceptions的优势

既然您知道了什么是异常以及如何使用它们,现在是时候学习在程序中使用异常的好处了。

1、优势1:将错误处理代码与“常规”代码分离

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

  1. readFile {
  2. open the file;
  3. determine its size;
  4. allocate that much memory;
  5. read the file into memory;
  6. close the file;
  7. }

乍一看,该函数似乎足够简单,但它忽略了所有以下潜在错误:

  • 如果无法打开文件,会发生什么情况?
  • 如果无法确定文件的长度,会发生什么情况?
  • 如果无法分配足够的内存,会发生什么情况?
  • 如果读取失败会发生什么情况?
  • 如果无法关闭文件,会发生什么情况?

要处理这种情况,readFile函数必须有更多的代码来执行错误检测、报告和处理。下面是函数的外观示例。

  1. errorCodeType readFile {
  2. initialize errorCode = 0;
  3. open the file;
  4. if (theFileIsOpen) {
  5. determine the length of the file;
  6. if (gotTheFileLength) {
  7. allocate that much memory;
  8. if (gotEnoughMemory) {
  9. read the file into memory;
  10. if (readFailed) {
  11. errorCode = -1;
  12. }
  13. } else {
  14. errorCode = -2;
  15. }
  16. } else {
  17. errorCode = -3;
  18. }
  19. close the file;
  20. if (theFileDidntClose && errorCode == 0) {
  21. errorCode = -4;
  22. } else {
  23. errorCode = errorCode and -4;
  24. }
  25. } else {
  26. errorCode = -5;
  27. }
  28. return errorCode;
  29. }

这里有太多的错误检测、报告和返回,以至于最初的七行代码都丢失在混乱中。更糟糕的是,代码的逻辑流也丢失了,因此很难判断代码是否在做正确的事情:如果函数未能分配足够的内存,文件是否真的被关闭?当您在编写方法三个月后修改它时,更难确保代码继续做正确的事情。许多程序员通过简单地忽略它来解决这个问题——当他们的程序崩溃时会报告错误。
异常使您能够编写代码的主流,并在其他地方处理异常情况。如果readFile函数使用异常而不是传统的错误管理技术,则它看起来更像下面这样。

  1. readFile {
  2. try {
  3. open the file;
  4. determine its size;
  5. allocate that much memory;
  6. read the file into memory;
  7. close the file;
  8. } catch (fileOpenFailed) {
  9. doSomething;
  10. } catch (sizeDeterminationFailed) {
  11. doSomething;
  12. } catch (memoryAllocationFailed) {
  13. doSomething;
  14. } catch (readFailed) {
  15. doSomething;
  16. } catch (fileCloseFailed) {
  17. doSomething;
  18. }
  19. }

请注意,异常不会使您节省检测、报告和处理错误的工作,但它们确实有助于您更有效地组织工作。

2、优势2:将错误向上传播到调用堆栈

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

  1. method1 {
  2. call method2;
  3. }
  4. method2 {
  5. call method3;
  6. }
  7. method3 {
  8. call readFile;
  9. }

还假设method1是唯一对readFile中可能发生的错误感兴趣的方法。传统的错误通知技术强制method2和method3将readFile返回的错误代码向上传播到调用堆栈,直到错误代码最终到达method1——唯一对它们感兴趣的方法。

  1. method1 {
  2. errorCodeType error;
  3. error = call method2;
  4. if (error)
  5. doErrorProcessing;
  6. else
  7. proceed;
  8. }
  9. errorCodeType method2 {
  10. errorCodeType error;
  11. error = call method3;
  12. if (error)
  13. return error;
  14. else
  15. proceed;
  16. }
  17. errorCodeType method3 {
  18. errorCodeType error;
  19. error = call readFile;
  20. if (error)
  21. return error;
  22. else
  23. proceed;
  24. }

回想一下,Java运行时环境向后搜索调用堆栈,以查找对处理特定异常感兴趣的任何方法。方法可以躲避其中抛出的任何异常,从而允许调用堆栈更上层的方法捕获它。因此,只有关心错误的方法才需要担心检测错误。

  1. method1 {
  2. try {
  3. call method2;
  4. } catch (exception e) {
  5. doErrorProcessing;
  6. }
  7. }
  8. method2 throws exception {
  9. call method3;
  10. }
  11. method3 throws exception {
  12. call readFile;
  13. }

然而,正如伪代码所示,避免异常需要中间人方法的一些努力。可以在方法内引发的任何已检查异常都必须在其throws子句中指定。

3、优势3:分组和区分错误类型

由于程序中抛出的所有异常都是对象,因此异常的分组或分类是类层次结构的自然结果。Java平台中一组相关异常类的示例是在Java.io-IOException及其后代中定义的异常类。IOException是最常见的,表示执行I/O时可能发生的任何类型的错误。它的后代表示更具体的错误。例如,FileNotFoundException表示在磁盘上找不到文件。
方法可以编写可以处理非常特定的异常的特定处理程序。FileNotFoundException类没有后代,因此以下处理程序只能处理一种类型的异常。

  1. catch (FileNotFoundException e) {
  2. ...
  3. }

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

  1. catch (IOException e) {
  2. ...
  3. }

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

  1. catch (IOException e) {
  2. // Output goes to System.err.
  3. e.printStackTrace();
  4. // Send trace to stdout.
  5. e.printStackTrace(System.out);
  6. }

您甚至可以使用这里的处理程序设置一个异常处理程序来处理任何异常。

  1. // A (too) general exception handler
  2. catch (Exception e) {
  3. ...
  4. }

Exception类接近Throwable类层次结构的顶部。因此,除了处理程序要捕获的异常之外,该处理程序还将捕获许多其他异常。例如,如果您希望程序执行的所有操作都是为用户打印错误消息,然后退出,则可能希望以这种方式处理异常。
然而,在大多数情况下,您希望异常处理程序尽可能具体。原因是处理程序必须做的第一件事是确定发生了哪种类型的异常,然后才能决定最佳恢复策略。实际上,通过不捕获特定错误,处理程序必须适应任何可能性。过于通用的异常处理程序可以通过捕获和处理程序员没有预料到的异常以及处理程序不打算处理的异常,使代码更容易出错。
如前所述,您可以创建异常组并以常规方式处理异常,也可以使用特定的异常类型来区分异常并以精确的方式处理异常。

二、总结

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

程序可以通过使用try、catch和finally块的组合来捕获异常。

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

try语句应至少包含一个catch块或finally块,并且可以有多个catch模块。
异常对象的类指示引发的异常的类型。异常对象可以包含有关错误的更多信息,包括错误消息。使用异常链,异常可以指向导致它的异常,而该异常又可以指向导致该异常的异常,以此类推。 

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/article/detail/44553
推荐阅读
相关标签
  

闽ICP备14008679号