Checked vs. Unchecked Exceptions: The Debate Is Not Over

The following text is a partial translation of the original English article, performed by ChatGPT (gpt-3.5-turbo) and this Jekyll plugin:

我们真的需要检查异常吗?辩论已经结束了,不是吗?对我来说并非如此。虽然大多数面向对象的语言都没有它们,而且大多数程序员认为检查异常是Java的错误,但我相信相反的观点——不受检查的异常才是错误。此外,我还认为多个异常类型也是一个坏主意。

让我首先解释一下我对面向对象编程中异常的理解。然后,我将把我的理解与“传统”方法进行比较,并讨论它们之间的区别。所以,首先是我的理解。

假设有一个方法可以将一些二进制数据保存到文件中:

public void save(File file, byte[] data)
  throws Exception {
  // save data to the file
}

当一切顺利时,该方法只是保存数据并返回控制权。当出现问题时,它会抛出“异常”,我们必须对此进行处理:

try {
  save(file, data);
} catch (Exception ex) {
  System.out.println("Sorry, we can't save right now.");
}

当一个方法说它“抛出”异常时,我理解该方法是不安全的。它有时可能会失败,我有责任要么1)处理这个失败,要么2)声明自己也是不安全的。

我知道每个方法都是根据单一责任原则进行设计的。这对我来说是一个保证,如果方法 save() 失败,它意味着整个保存操作无法完成。如果我需要知道这个失败的原因是什么,我将会解开(un-chain)异常——遍历异常链和在 ex 中封装的堆栈跟踪。

我从不使用异常来控制流程,这意味着我从不恢复发生异常的情况。当异常发生时,我让它传递到应用程序的最高级别。有时候,我重新抛出异常以添加更多的语义信息到异常链中。这就是为什么对于save()抛出的异常原因对我来说并不重要。我只知道方法失败了,对我来说这已经足够了。总是如此。

出于同样的原因,我不需要区分不同的异常类型。我只需要那种层次结构。对我来说,Exception就足够了。再次强调,那是因为我不使用异常来控制流程。

这是我理解异常的方式。

根据这个范式,我会说我们必须:

  • 始终使用已检查异常。

  • 永远不要抛出/使用未经检查的异常。

  • 只使用 Exception,不使用任何子类型。

  • throws块中始终声明一个异常类型。

  • 永远不要捕获异常而不重新抛出;请在此处阅读更多相关内容。

这种模式与我在这个主题上找到的许多其他文章不同。让我们进行比较和讨论。

Runtime vs. API Exceptions

Oracle says 一些异常应该是 API 的一部分(被检查的异常),而一些是运行时异常,不应该是 API 的一部分(未检查的异常)。它们将在 JavaDoc 中进行文档化,但不会在方法签名中显示。

我不理解这里的逻辑,我相信Java的设计者们也不理解。为什么有些异常很重要,而其他异常却不重要?为什么有些异常在方法签名的throws块中应该有适当的API位置,而其他异常则没有?标准是什么?

我这里有一个答案。通过引入已检查和未检查的异常,Java开发人员试图解决方法过于复杂和混乱的问题。当一个方法太大且同时做了太多事情(违反了单一职责原则)时,让我们保留一些异常“隐藏”(也称为未检查异常)肯定是更好的选择。但这并不是一个真正的解决方案。它只是一个暂时的补丁,对我们所有人都带来了更多的伤害——方法的规模和复杂性不断增加。

未经检查的异常是Java设计中的错误,而不是经过检查的异常。

隐藏一个方法可能在某些情况下失败的事实是一个错误。这正是未检查异常所做的。

相反,我们应该让这个事实变得明显。当一个方法做了太多的事情时,会有太多的故障点,方法的作者会意识到有些地方出错了–一个方法不应该在这么多情况下抛出异常。这将导致重构。未经检查的异常的存在会导致一团糟。顺便说一下,在 Ruby、C#、Python、PHP 等语言中根本就不存在已检查的异常。这意味着这些语言的创建者对面向对象编程的理解甚至比不上 Java 的作者。

Checked Exceptions Are Too Noisy

另一个常见反对受检异常的论点是它们使我们的代码更冗长。我们必须在各处放置 try/catch ,而不是专注于主要逻辑。Bozhidar Bozhanov 甚至 建议 了一个针对这种冗长问题的技术解决方案。

再说一遍,我不明白这个逻辑。如果在方法save()出错时我想要做一些操作,我会捕获异常并以某种方式处理这种情况。如果我不想这样做,我只需要声明我的方法也会throws异常,然后就不再关注异常处理。问题出在哪里?为什么会显得冗长啰嗦?

我也有一个答案。它来自于未经检查的异常的存在。我们无法总是忽略失败,因为我们使用的接口不允许我们这样做。仅此而已。例如,Runnable类,它被广泛用于多线程编程,其run()方法不应该抛出任何异常。这就是为什么我们总是必须在方法内部捕获一切异常,并将已检查的异常重新抛出为未经检查的异常。

如果所有Java接口中的所有方法都声明为“安全”(不抛出任何异常)或“不安全”(抛出异常),那么一切将变得合乎逻辑和清晰明了。如果您想保持“安全”,就要承担失败处理的责任。否则,选择“不安全”,让用户自行担心安全问题。

没有噪音,非常整洁的代码,以及明显的逻辑。

Inappropriately Exposed Implementation Details

有人说 在方法签名中将已检查异常放入throws而不是在此处捕获并重新抛出新类型的能力,鼓励我们在方法签名中拥有太多不相关的异常类型。例如,我们的方法save()可能声明它可能抛出OutOfMemoryException,即使它似乎与内存分配无关。但它确实分配了一些内存,对吧?所以在文件保存操作期间可能会发生内存溢出。

再次说,我不明白这个论点的逻辑。如果所有的异常都被检查,并且我们没有多个异常类型,那么我们只需在任何地方抛出“Exception”就可以了。为什么我们需要在第一时间关注异常类型呢?如果我们不使用异常来控制流程,我们就不会这样做。

如果我们真的想让我们的应用程序在内存溢出方面具备抵抗能力,我们将引入一些内存管理器,它将具有类似于 bigEnough() 方法的功能,该方法将告诉我们我们的堆是否足够大以执行下一个操作。在这种情况下使用异常是面向对象编程中完全不合适的异常处理方法。

Recoverable Exceptions

Effective Java 中,Joshua Bloch 說「對於可恢復的狀況使用檢查例外,對於程式錯誤使用運行時例外」。他的意思是這樣的:

try {
  save(file, data);
} catch (Exception ex) {
  // We can't save the file, but it's OK
  // Let's move on and do something else
}

这与一个名为“不要使用异常进行流程控制”的著名反模式有何不同?Joshua,恕我直言,你是错的。在面向对象编程中,不存在可恢复的条件。异常表示从一个方法到另一个方法的调用链被中断,现在是时候沿着链条向上走并停在某个地方了。但是在异常之后我们不会再返回原处。

App#run()
  Data#update()
    Data#write()
      File#save() <-- Boom, there's a failure here, so we go up

我们可以重新开始这个链,但是在 throw 之后我们不会回退。换句话说,在 catch 块中我们不会执行任何操作。我们只报告问题并结束执行。我们永远不会“恢复!”


All arguments against checked exceptions demonstrate nothing but a serious misunderstanding of object-oriented programming by their authors. The mistake in Java and in many other languages is the existence of unchecked exceptions, not checked ones.

Translated by ChatGPT gpt-3.5-turbo/35 on 2023-09-09 at 05:49

sixnines availability badge   GitHub stars