一尘不染

Java针对检查异常的情况

java

多年来,我一直无法获得以下问题的正确答案:为什么某些开发人员反对受检查的异常?我进行了无数对话,阅读了博客上的东西,阅读了布鲁斯·埃克尔(Bruce Eckel)所说的话(我看到的第一个反对他们的人)。

我目前正在编写一些新代码,并非常注意我如何处理异常。我试图看到“我们不喜欢检查的异常”人群的观点,但我仍然看不到它。

我的每一次对话都以相同的问题为结尾而结束…让我进行设置:

一般而言(根据Java的设计方式),

  • Error 用于永远不应该抓到的东西(VM有花生过敏症,有人在上面撒了一罐花生)
  • RuntimeException 用于程序员犯错的事情(程序员走出了数组的结尾)
  • Exception(除外RuntimeException)用于程序员无法控制的事情(写入文件系统时磁盘已满,已达到该进程的文件句柄限制,并且您无法打开其他文件)
  • Throwable 只是所有异常类型的父级。

我听到的一个常见论点是,如果发生异常,那么开发人员要做的就是退出程序。

我听到的另一个常见论点是,检查异常会使重构代码变得更加困难。

对于“我要做的就是退出”参数,我说即使您要退出,也需要显示一条合理的错误消息。如果您只是在处理错误,那么当程序退出时如果没有明确说明原因的话,您的用户不会过分高兴。

对于“它很难重构”人群,这表明未选择适当的抽象级别。与其声明一个方法不会抛出IOException,还不如IOException将其转换为更适合所发生情况的异常。

我在将Maincatch(Exception)(或在某些情况下catch(Throwable)确保程序可以正常退出)进行包装时没有问题,但我始终会捕获所需的特定异常。这样做至少可以显示一个适当的异常。错误信息。

人们从不回覆的问题是:

如果您抛出RuntimeException 子类而不是Exception子类,那么您如何知道应该掌握什么呢?

如果答案是Exception问题,那么您也将以与系统异常相同的方式处理程序员错误。对我来说这似乎是错误的。

如果您发现了问题,Throwable那么您将以相同的方式处理系统异常和VM错误(等等)。对我来说这似乎是错误的。

如果答案是仅捕获已知的异常,那么您如何知道抛出了哪些异常?当程序员X抛出一个新异常而忘记捕捉它时,会发生什么?对我来说这很危险。

我会说一个显示堆栈跟踪的程序是错误的。不喜欢检查异常的人会不会有这种感觉?

因此,如果您不喜欢检查异常,是否可以解释为什么不这样做并回答未得到回答的问题?

编辑:我不是在寻找有关何时使用任何一种模型的建议,我正在寻找的是人们为什么从中扩展,RuntimeException因为他们不喜欢从中扩展Exception和/或为什么他们捕获异常,然后重新抛出RuntimeException而不是向其添加抛出他们的方法。我想了解不喜欢检查异常的动机。


阅读 270

收藏
2020-03-06

共1个答案

一尘不染

我想我读过与您同样的Bruce Eckel访谈-这总是困扰我。实际上,争论是由受访者提出的(如果确实是您所谈论的帖子),Anders Hejlsberg,.NET和C#的MS天才。

http://www.artima.com/intv/handcuffs.html

范虽然我是海斯伯格及其工作人员,但这种说法一直使我感到虚假。基本上可以归结为:

“受检查的异常是不好的,因为程序员只是通过始终捕获并消除它们来滥用它们,这导致问题被隐藏和忽略,否则将呈现给用户。”

通过“以其他方式呈现给用户的”我的意思是,如果你使用一个运行时异常懒惰的程序员会忽略它(而不是一个空的catch块捕获它),用户将看到它。

论点摘要的摘要是“程序员不会正确使用它们,而没有正确使用它们比没有它们更糟糕”。

这个论点有些道理,实际上,我怀疑高斯林不将运算符覆盖在Java中的动机来自类似的论点-它们使程序员感到困惑,因为它们经常被滥用。

但最后,我发现这是对海斯伯格的虚假论证,可能是后来提出的一种解释,以解释这种缺乏而不是经过深思熟虑的决定。

我认为,尽管过度使用检查异常是一件坏事,并且会导致用户草率处理,但是正确使用它们可以使API程序员为API客户端程序员带来巨大的好处。

现在,API程序员必须小心,不要到处抛出检查过的异常,否则它们只会惹恼客户程序员。(Exception) {}当Hejlsberg警告时,非常懒惰的客户端程序员将求助于追赶者,所有利益都将丢失,随之而来的是地狱。但是在某些情况下,无法替代良好的检查异常。

对我而言,经典示例是文件打开API。语言历史中的每种编程语言(至少在文件系统上)在某处都有一个API,可让您打开文件。每个使用此API的客户端程序员都知道他们必须处理自己尝试打开的文件不存在的情况。让我重新表述一下:每个使用此API的客户端程序员都应该知道他们必须处理这种情况。麻烦之处在于:API程序员可以帮助他们知道应该通过单独注释来处理它,还是可以确实要求客户端处理它。

在C语言中,成语类似于

  if (f = fopen("goodluckfindingthisfile")) { ... } 
  else { // file not found ...

其中fopen通过返回0和C(愚蠢地)表示失败,这让您将0视为布尔值,并且…基本上,您了解了这个习惯用法,您还可以。但是,如果您是菜鸟,却没有学习成语怎么办。然后,当然,您从

   f = fopen("goodluckfindingthisfile");
   f.read(); // BANG! 

并学习困难的方法。

请注意,我们在这里只谈论强类型语言:对强类型语言中的API有了一个清晰的认识:它是功能(方法)的杂类,可供您为每个语言使用明确定义的协议。

明确定义的协议通常由方法签名定义。在这里,fopen要求您向其传递一个字符串(对于C,则为char *)。如果您给它其他东西,则会得到编译时错误。您未遵循协议-您未正确使用API​​。

在某些(晦涩的)语言中,返回类型也是协议的一部分。如果您尝试fopen()在某些语言中调用等价的而不将其分配给变量,则还会收到编译时错误(您只能使用void函数来做到这一点)。

我要说明的重点是:在一种静态类型的语言中,API程序员鼓励客户端正确地使用API​​,方法是防止客户端代码犯任何明显的错误而进行编译。

(在像Ruby这样的动态类型的语言中,您可以传递任何内容(例如浮点数)作为文件名-并且它将进行编译。如果您什至不打算控制方法参数,为什么还要用受检查的异常来烦扰用户。此处的参数仅适用于静态类型的语言。)

那么,检查异常呢?

好了,这里是您可以用来打开文件的Java API之一。

try {
  f = new FileInputStream("goodluckfindingthisfile");
}
catch (FileNotFoundException e) {
  // deal with it. No really, deal with it!
  ... // this is me dealing with it
}

看到那个收获了吗?这是该API方法的签名:

public FileInputStream(String name)
                throws FileNotFoundException

请注意,这FileNotFoundException是一个检查的异常。

API程序员对您说:“您可以使用此构造函数创建新的FileInputStream,但您可以

a)必须将文件名作为字符串传递
b)必须接受在运行时找不到文件的可能性”

就我而言,这就是重点。

关键基本上是该问题所说的“程序员无法控制的事情”。我首先想到的是,他/她的意思超出了API程序员的控制范围。但是实际上,如果正确使用了检查异常,则实际上应该是由客户端程序员和API程序员无法控制的事情。我认为这是不滥用检查异常的关键。

我认为文件打开很好地说明了这一点。API程序员知道您可能给他们提供了一个在调用API时不存在的文件名,并且他们将无法返回您想要的内容,但是将引发异常。他们还知道,这将非常定期地发生,并且客户程序员可能会在编写调用时期望文件名正确,但是由于超出其控制范围的原因,它在运行时也可能是错误的。

因此,API明确表明:在某些情况下,在您致电给我时此文件不存在,您该死的更好了。

用反例会更清楚。想象一下我正在编写一个表API。我在某个地方有一个包含此方法的API的表模型:

public RowData getRowData(int row) 

现在,作为一名API程序员,我知道在某些情况下某些客户端会将行的负值或表外的行值传递给它。因此,我可能很想抛出一个检查的异常并强制客户端对其进行处理:

public RowData getRowData(int row) throws CheckedInvalidRowNumberException

(当然,我不会真正将其称为“已检查”。)

这是对检查异常的不当使用。客户端代码将充满调用以获取行数据,每个调用都必须使用try / catch,为什么?他们是否要向用户报告错误的行?可能不是-因为无论我的表格视图周围的UI是什么,它都不应该使用户进入请求非法行的状态。因此,这是客户端程序员的一个错误。

API程序员仍然可以预测客户端将对此类错误进行编码,并应使用诸如的运行时异常来处理它IllegalArgumentException

有了中的检查异常getRowData,显然这将导致Hejlsberg的惰性程序员只是添加空的catch。发生这种情况时,即使对测试人员或客户端开发人员进行调试,非法的行值也不会很明显,而是它们会导致连锁错误,很难查明错误的来源。阿丽亚娜火箭将在发射后炸毁。

好的,这就是问题所在:我说的是,检查异常FileNotFoundException不仅是一件好事,而且还是API程序员工具箱中用于以对客户端程序员最有用的方式定义API的基本工具。但是,CheckedInvalidRowNumberException这带来了很大的不便,导致编程错误,应避免使用。但是如何区分。

我想这不是一门精确的科学,我想这是海斯伯格论点的基础,也许在一定程度上是合理的。但是我不乐意在这里把婴儿和洗澡水一起扔出去,所以让我在这里提取一些规则,以区分好检查的异常与坏的:

  1. 超出客户的控制范围或封闭与开放:

仅当错误情况不受API 和客户端编程器的控制时,才应使用检查的异常。这与系统的打开或关闭程度有关。在受限的 UI中,客户端程序员可以控制所有从表视图添加和删除行的按钮,键盘命令等(封闭系统),如果尝试从中获取数据,则是客户端编程错误。不存在的行。在任何数量的用户/应用程序都可以添加和删除文件的基于文件的操作系统中(开放系统),可以想象到客户端请求的文件已被删除而没有他们的知识,因此应该期望他们处理。

  1. 无处不在:

客户端频繁进行的API调用上不应使用已检查的异常。通常,我指的是客户端代码中的很多地方-时间不多。因此,客户端代码通常不会尝试大量打开同一文件,但是我的表视图可以RowData通过不同的方法遍地开花。特别是,我将要编写很多代码,例如

if (model.getRowData().getCell(0).isEmpty())

而且每次都要尝试/捕获都非常麻烦。

  1. 通知用户:

在您可以想象向终端用户呈现有用的错误消息的情况下,应使用检查异常。这就是“发生这种情况时您会怎么做?” 我在上面提出的问题。它还与第1项相关。由于您可以预测到客户端API系统外部的某些内容可能会导致文件不存在,因此您可以合理地告知用户:

"Error: could not find the file 'goodluckfindingthisfile'"

由于您的非法行号是由内部错误引起的,并且没有用户的过错,因此实际上没有任何有用的信息可提供给他们。如果您的应用程序不允许运行时异常进入控制台,那么它可能最终会给他们一些难看的消息,例如:

"Internal error occured: IllegalArgumentException in ...."

简而言之,如果您认为客户程序员不能以对用户有用的方式解释异常,那么您可能不应该使用检查异常。

这就是我的规则。人为地作了准备,毫无疑问会有例外(如果可以的话,请帮助我完善它们)。但是我的主要论点是,在某些情况下,诸如FileNotFoundExceptionAPI契约的一部分所检查的异常与参数类型一样重要和有用。因此,我们不应仅仅因为滥用而放弃它。

抱歉,这并不意味着要花那么长时间。让我最后提出两个建议:

答:API程序员:请谨慎使用检查异常,以保留其有用性。如有疑问,请使用未经检查的异常。

B:客户程序员:养成在开发初期就创建包装异常(使用Google封装)的习惯。JDK 1.4和更高版本RuntimeException为此提供了一个构造函数,但是您也可以轻松创建自己的构造函数。这是构造函数:

public RuntimeException(Throwable cause)

然后养成这样的习惯:每当您必须处理受检查的异常并且感到懒惰(或者您认为API程序员首先使用受检查的异常过于热心)时,不要仅仅吞下该异常,将其包装起来然后扔掉

try {
  overzealousAPI(thisArgumentWontWork);
}
catch (OverzealousCheckedException exception) {
  throw new RuntimeException(exception);  
}

将其放在您的IDE的一个小代码模板中,并在感到懒惰时使用它。这样,如果您确实需要处理检查的异常,则在运行时看到问题后,您将不得不返回并处理该异常。因为,相信我(和Anders Hejlsberg),您永远都不会再回到您的TODO

catch (Exception e) { /* TODO deal with this at some point (yeah right) */}
2020-03-06