辩论逻辑_例外辩论

像C ++一样,Java语言提供了引发和捕获异常的功能。 但是,与C ++不同,Java语言支持检查和未检查的异常。 Java类必须声明它们在方法签名中抛出的所有已检查异常,并且任何调用引发E类型的已检查异常的方法的方法都必须捕获E或也必须声明为抛出E(或E的超类)。 这样,该语言迫使我们记录所有可能退出控制方法的预期方式。

使开发人员可以处理由于编程错误而导致的异常或无法期望程序捕获的异常(解引用空指针,从数组末尾掉落,除以零等)。 ,某些异常被提名为未经检查的异常(那些源自RuntimeException异常),不需要声明。

一般常识

以下摘录自Sun的“ The Java Tutorial”摘录中总结了将异常声明为选中还是未选中的常规知识( 有关更多信息,请参见参考资料 ):

因为Java语言不需要捕获或指定运行时异常的方法,所以程序员倾向于编写仅引发运行时异常的代码,或者使它们的所有异常子类都继承自RuntimeException 这两种编程快捷方式均允许程序员编写Java代码,而不必理会来自编译器的所有烦人错误,也不必理会指定或捕获任何异常。 尽管这对于程序员来说似乎很方便,但它避开了Java捕获或指定要求的意图,并可能给使用您的类的程序员带来麻烦。

经检查的异常代表有关合法指定请求的操作的有用信息,该请求可能是呼叫者无法控制的,并且需要告知呼叫者-例如,文件系统现在已满,或者远端已关闭该请求。连接,或者访问权限不允许执行此操作。

是什么给你买,如果你抛出RuntimeException或创建的子类RuntimeException ,因为你不希望处理规定吗? 简而言之,您无需指定就可以引发异常。 换句话说,这是一种避免记录方法可能引发的异常的方法。 什么时候好? 那么,什么时候避免记录方法的行为永远是好的? 答案是“几乎没有”。

换句话说,Sun告诉我们,检查异常应该成为规范。 本教程以几种不同的方式继续说,除非您是JVM,否则通常应该抛出Exception而不是RuntimeException

Josh Bloch在他的《 有效的Java:编程语言指南》 (请参阅参考资料 )中,提供了有关已检查和未检查的异常的以下几点知识,这些知识与“ Java教程”的建议一致(但不够严格):

  • 项目39:仅在特殊情况下使用例外。 也就是说,请勿对控制流使用异常,例如在调用Iterator.next()时捕获NoSuchElementException而不是先检查Iterator.hasNext()
  • 项目40:将可检查的异常用于可恢复的条件,将运行时异常用于编程错误。 在这里,布洛赫(Bloch)呼应了传统的Sun智慧-运行时异常仅应用于指示编程错误,例如违反先决条件。
  • 项目41:避免不必要地使用检查的异常。 换句话说,对于调用者可能无法恢复的条件,或者对于唯一可预见的响应是程序退出的条件,不要使用检查的异常。
  • 项目43:抛出适合于抽象的异常。 换句话说,方法引发的异常应在与该方法的作用一致的抽象级别上定义,而不必在其实现方式的低级详细信息上进行定义。 例如,从文件,数据库或JNDI加载资源的方法在找不到资源时(通常使用异常链接来保留潜在原因)而不是较低级别的IOExceptionSQLException ,应该抛出某种ResourceNotFound异常。或NamingException

重新检查检查的异常正统

最近,布鲁斯·埃克尔(Bruce Eckel)和罗德·约翰逊(Rod Johnson)等几位广受好评的专家公开表示,尽管他们最初完全同意关于检查异常的正统立场,但他们得出的结论是,仅使用检查异常并不是一个好主意。一开始就出现了,对于许多大型项目来说,检查异常已经成为问题的重要根源。 埃克尔(Eckel)采取了更为极端的观点,建议应不检查所有异常。 Johnson的观点较为保守,但仍然表明对检查异常的正统偏好过高。 (值得注意的是,几乎肯定拥有使用Java技术的丰富经验的C#架构师选择从语言设计中省略检查异常,使所有异常都成为未检查异常。但是,他们确实为检查异常的实现留出了空间。晚些时候。)

对检查异常的一些批评

Eckel和Johnson都指出了带有已检查异常的类似问题列表。 有些是检查异常的固有属性,有些是Java语言中检查异常的特定实现的属性,而有些只是关于广泛滥用检查异常的严重性的观察,以至于可能需要重新考虑该机制。 。

检查的异常不适当地公开了实现细节

您看过(或写过)几次抛出SQLExceptionIOException的方法,即使该方法似乎与数据库或文件无关? 对于开发人员来说,很简单地将所有可能在方法的初始实现中引发的异常汇总起来并将其添加到方法的throws子句中是很常见的(许多IDE甚至可以帮助您执行此任务)。 传递方法的一个问题是它违反了Bloch的第43项-所引发的异常处于与引发它们的方法不一致的抽象级别。

一种用于加载用户配置文件的方法,当它找不到用户而不是SQLException时,应该引发NoSuchUserException -调用者很可能希望找不到该用户,但不知道该如何处理SQLException 异常链接可用于引发更适当的异常,而不会丢弃基础故障的详细信息(例如堆栈跟踪),从而允许抽象层将其上方的层与它们下方的层的细节隔离,同时保留可能的信息。对于调试很有用。

也就是说,像JDBC这样的软件包的设计方式使其很难避免此问题。 JDBC接口中的每个方法都会引发SQLException ,但是在访问数据库的过程中可能会遇到几种不同类型的问题,并且不同的方法可能会遇到不同的错误模式。 SQLException可能指示系统级问题(无法建立与数据库的连接),逻辑问题(结果集中没有更多行)或特定数据问题(您刚尝试行的主键)插入已经存在或违反实体完整性约束)。 调用者无法在SQLException的这些不同类型之间进行区分,而不会犯试图解析消息文本的不可原谅的罪过。 SQLException确实公开了用于获取特定于数据库的错误代码和SQL状态变量的方法,但实际上,这些方法很少用于区分不同的数据库错误情况。)

方法签名不稳定

方法签名不稳定的问题与先前的问题有关-如果您只是通过方法传递异常,那么每次更改方法的实现时,都必须更改其方法签名,并更改所有叫它。 一旦在生产中已经部署了类,则管理易碎方法签名将成为一项昂贵的提议。 但是,此问题基本上是未能遵循Bloch的第43项的另一个症状。方法遇到故障时应该抛出异常,但是该异常应该反映该方法的作用,而不是方法的作用。

有时,当程序员厌倦了由于实现更改而从方法签名中添加和删除异常时,他们没有使用抽象定义给定层可能抛出的异常类型,而是简单地声明了所有方法来抛出Exception 换句话说,他们认为异常实在是太痛苦了,并基本上将其关闭。 不用说,对于大多数人来说,这种方法通常不是一个好的错误处理策略,而是最易处理的代码。

无法读取的代码

由于许多方法会抛出许多不同的异常,因此错误处理代码与实际操作代码的行数之比可能很高,这使得很难找到在方法中实际执行某些操作的代码。 异常应该通过集中错误处理来使代码更小,但是具有三行代码和六个catch块的方法(每个块要么只是记录异常,要么将其包装然后重新抛出)似乎有点肿,并且可以混淆其他简单的代码。

吞咽异常

我们都已经看到了捕获了异常的代码,但是catch块内部没有代码。 尽管这种编程实践显然是不好的做法,但很容易看出来是怎么回事-在原型制作过程中,有人用try...catch块包装了代码,却忘记了稍后再返回并填充catch块。 尽管此错误是一个常见错误,但它也是更好的工具可以为我们节省的地方之一-编辑器,编译器或静态检查工具很容易在吞咽异常的情况下检测并发出警告。

过于笼统的try...catch块是吞咽异常的另一种形式,它很难检测到,并且是Java类库中异常类层次结构的(可疑)结构的结果。 假设一个方法抛出四种不同类型的异常,遇到任何异常的调用方将捕获它们,记录它们并返回。 一种实现此策略的方法是使用带有四个catch子句的try...catch块,每种子句分别对应一种异常。 为了避免不可读的代码问题,一些开发人员将重构该代码,如清单1所示:

清单1.意外吞下RuntimeException
try { 
  doSomething();
}
catch (Exception e) { 
  log(e);
}

尽管此代码比四个catch块更紧凑,但它有一个问题-它也捕获了doSomething可能抛出的任何RuntimeException ,并阻止了它们的传播。

异常包装过多

如果异常是在低级设施中生成的,并且通过多层代码传播,则在最终处理该异常之前,可能会对其进行多次捕获,包装和重新抛出。 当最终记录异常时,堆栈跟踪可能会包含许多页面,因为堆栈跟踪将被重复多次,每层环绕一次。 (在JDK 1.4及更高版本中,异常链接的实现在某种程度上缓解了此问题。)

替代方法

Bruce Eckel着Java中 (参见思维的作者相关信息 )说,经过多年使用Java语言的,他已经到了该检查的异常是一个错误的结论-即应被宣布失败的实验。 Eckel提倡使所有异常都处于非检查状态,并提供清单2中的类作为将已检查的异常转换为未检查的异常的一种方法,同时保留了捕获在堆栈中传播的特定类型异常的能力(请参阅参考资料部分中的文章)有关如何使用它的说明):

清单2. Eckel的异常适配器类
class ExceptionAdapter extends RuntimeException {
  private final String stackTrace;
  public Exception originalException;
  public ExceptionAdapter(Exception e) {
    super(e.toString());
    originalException = e;
    StringWriter sw = new StringWriter();
    e.printStackTrace(new PrintWriter(sw));
    stackTrace = sw.toString();
  }
  public void printStackTrace() { 
    printStackTrace(System.err);
  }
  public void printStackTrace(java.io.PrintStream s) { 
    synchronized(s) {
      s.print(getClass().getName() + ": ");
      s.print(stackTrace);
    }
  }
  public void printStackTrace(java.io.PrintWriter s) { 
    synchronized(s) {
      s.print(getClass().getName() + ": ");
      s.print(stackTrace);
    }
  }
  public void rethrow() { throw originalException; }
}

如果您关注Eckel网站上的讨论,您会发现响应者的分歧很大。 有人认为他的提议很荒谬。 有人认为这是个好主意。 (我的观点是,尽管正确使用例外肯定会带来挑战,并且存在大量错误使用例外的例子,但大多数同意他的人都是出于错误的原因这样做的,就像政治家在竞选活动中跑来跑去一样。普遍获得巧克力补贴的平台将获得10岁儿童的很多选票。)

J2EE Design and Development的作者Rod Johnson(请参阅参考资料 )是我读过的关于Java开发的最佳书籍之一,无论是否J2EE,它都采用了一种不太激进的方法。 他列举了几类异常,并为每种异常确定了一种策略。 有些例外基本上是辅助返回码(通常表示违反了业务规则),而有些例外是“发生了严重错误”(例如无法建立数据库连接)。 Johnson主张对第一类使用检查异常(替代返回码),对后一类使用运行时异常。 在“发生严重错误的情况”类别中,其动机仅是要认识到以下事实:没有任何调用者会有效地处理此异常,因此它也可能一直传播到整个堆栈中,而对介入的影响最小代码(并最大程度地减少吞下异常的机会)。

约翰逊还列举了一个中间立场,他提出了一个问题:“只有少数呼叫者愿意处理此问题吗?” 对于这些情况,他还建议使用未经检查的异常。 作为该类别的示例,他列出了JDO异常-大多数情况下,JDO异常表示调用者不希望处理的条件,但是在某些情况下,可能会捕获并处理特定类型的异常。 他建议不要使用捕获和重新抛出这些异常的形式来使其余所有使用JDO的类为这种可能性付出代价,而是建议在此处使用未经检查的异常。

使用未经检查的异常

使用非检查异常的决定是一个复杂的决定,很明显,没有明显的答案。 Sun的建议是什么都不要使用它们,C#方法(Eckel和其他人都同意)是将它们用于所有东西。 其他人则说:“有中间立场。”

在C ++中使用了所有未检查的异常的异常之后,我发现未检查的异常的最大风险之一是它们没有像检查的异常那样自我记录。 除非API的创建者明确记录抛出的异常,否则调用者将无法知道要在其代码中捕获哪些异常。 不幸的是,我的经验是,大多数C ++ API的文档编写得很少,甚至文档齐全的API都没有足够的信息说明给定方法可能引发哪些异常。 我看不出有什么原因导致严重依赖未检查异常的Java类库不会普遍出现此问题。 依靠您自己或同事的编程技能已经很难了。 必须依靠任何人的文档编制技能(您可能会在调用堆栈中使用16帧的代码)作为主要错误处理机制,这是令人恐惧的。

文档问题进一步强调了为什么懒惰是选择使用未检查的异常的不好原因,因为使用未检查的异常的软件包的文档负担应该甚至比使用检查的异常还要高(因为记录您抛出的未检查的异常变得更加重要比已检查的例外)。

文件,文件,文件

如果您决定使用未检查的异常,则需要彻底记录此选择,包括在Javadoc中记录方法可能抛出的所有未检查的异常。 约翰逊建议逐个包地在已检查和未检查的异常之间做出决定。 使用未检查的异常时,还请记住,即使没有捕获到任何异常,也可能需要使用try...finally阻止,以便执行清除操作,例如关闭数据库连接。 对于已检查的异常,我们使用try...catch来提醒我们添加finally子句。 除非有未经检查的例外情况,否则我们不需要依靠。


翻译自: https://www.ibm.com/developerworks/java/library/j-jtp05254/index.html

你可能感兴趣的:(辩论逻辑_例外辩论)