ChatGPT解决这个技术问题 Extra ChatGPT

针对检查异常的案例

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

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

我的每一次谈话都以同样的问题结束……让我来设置一下:

一般来说(从 Java 的设计方式来看),

错误是针对不应该被捕获的东西(VM 对花生过敏,有人在上面掉了一罐花生)

RuntimeException 是针对程序员做错的事情(程序员离开了数组的末尾)

异常(RuntimeException 除外)是针对程序员无法控制的事情(写入文件系统时磁盘已满,已达到进程的文件句柄限制,您无法再打开任何文件)

Throwable 只是所有异常类型的父级。

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

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

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

对于“重构变得困难”的人群,这表明没有选择适当的抽象级别。与其声明一个方法抛出 IOException,不如将 IOException 转换为更适合正在发生的事情的异常。

我没有用 catch(Exception) 包装 Main (或在某些情况下 catch(Throwable) 以确保程序可以正常退出 - 但我总是捕获我需要的特定异常。这样做可以让我在至少,显示适当的错误消息。

人们从不回答的问题是:

如果你抛出 RuntimeException 子类而不是 Exception 子类,那么你怎么知道你应该捕获什么?

如果答案是 catch Exception,那么您也将像处理系统异常一样处理程序员错误。这对我来说似乎是错误的。

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

如果答案是你只捕获你知道抛出的异常,那么你怎么知道抛出了什么?当程序员 X 抛出一个新异常并忘记捕获它时会发生什么?这对我来说似乎很危险。

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

所以,如果你不喜欢检查的异常,你能解释一下为什么不回答没有得到回答的问题吗?

我不是在寻找关于何时使用这两种模型的建议,我在寻找的是为什么人们从 RuntimeException 扩展,因为他们不喜欢从 Exception 扩展和/或为什么他们抓住一个异常,然后重新抛出一个 RuntimeException 而不是在他们的方法中添加抛出。我想了解不喜欢检查异常的动机。

我不认为这完全是主观的——它是一种语言功能,旨在具有特定用途,而不是让每个人自己决定它的用途。而且它并不是特别有争议,它预先解决了人们很容易提出的具体反驳。
来吧。作为一种语言特征,这个话题已经并且可以以一种客观的方式来处理。
@cletus“回答你自己的问题”如果我有答案,我就不会问这个问题了!
好问题。在 C++ 中根本没有检查异常,在我看来,它使异常功能无法使用。你最终会陷入这样一种情况,即你必须在你所做的每一个函数调用中都设置一个 catch,因为你只是不知道它是否会抛出一些东西。
我所知道的关于检查异常的最有力论据是它们最初并不存在于 Java 中,并且当它们被引入时,它们在 JDK 中发现了数百个错误。这有点早于 Java 1.0。我个人不会没有他们,并且在这点上与 Bruce Eckel 和其他人强烈反对。

S
SørenHN

我想我读过和你一样的布鲁斯·埃克尔采访——它总是让我烦恼。事实上,这个论点是由 .NET 和 C# 背后的 MS 天才 Anders Hejlsberg(如果这确实是你正在谈论的帖子)提出的。

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

尽管我是 Hejlsberg 和他的作品的粉丝,但这个论点一直让我觉得是假的。它基本上归结为:

“检查的异常是不好的,因为程序员只是滥用它们,总是捕捉它们并解雇它们,这导致问题被隐藏和忽略,否则这些问题会呈现给用户”。

“以其他方式呈现给用户”是指如果您使用运行时异常,懒惰的程序员将忽略它(而不是用空的 catch 块捕获它)并且用户会看到它。

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

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

但最后,我发现这是 Hejlsberg 的一个虚假论点,并且可能是为了解释缺乏而创建的一个事后论点,而不是一个经过深思熟虑的决定。

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

现在 API 程序员必须小心不要到处抛出已检查的异常,否则它们只会惹恼客户端程序员。正如 Hejlsberg 警告的那样,非常懒惰的客户端程序员将求助于捕获 (Exception) {},所有好处都将丢失,地狱将接踵而至。但是在某些情况下,一个好的检查异常是无可替代的。

对我来说,经典的例子是文件打开 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 的懒惰程序员简单地添加空捕获。发生这种情况时,即使对测试人员或客户端开发人员进行调试,非法行值也不会很明显,而是会导致难以查明来源的连锁错误。阿里安火箭发射后会爆炸。

好的,问题来了:我说检查异常 FileNotFoundException 不仅是一件好事,而且是 API 程序员工具箱中的一个必不可少的工具,用于以对客户端程序员最有用的方式定义 API。但是 CheckedInvalidRowNumberException 是一个很大的不便,会导致糟糕的编程,应该避免。但是如何区分。

我想这不是一门精确的科学,我想这在一定程度上是 Hejlsberg 论点的基础,并且可能证明了这一点。但是我不乐意把婴儿和洗澡水一起扔在这里,所以请允许我在这里提取一些规则来区分好的检查异常和坏的:

Out of client's control or Closed vs Open: Checked exceptions 应该只在错误情况不受 API 和客户端程序员控制的情况下使用。这与系统的开放程度或封闭程度有关。在客户端程序员可以控制所有按钮、键盘命令等的受限 UI 中,这些按钮、键盘命令等从表视图(封闭系统)中添加和删除行,如果它试图从一个不存在的行。在基于文件的操作系统中,任何数量的用户/应用程序都可以添加和删除文件(开放系统),可以想象客户端请求的文件已在他们不知情的情况下被删除,因此应该期望他们处理它.普遍性:不应在客户端频繁进行的 API 调用上使用已检查的异常。经常我的意思是来自客户端代码中的很多地方 - 不是经常在时间上。因此,客户端代码不会经常尝试打开同一个文件,但我的表格视图通过不同的方法在各处获取 RowData。特别是,我将编写大量代码,例如 if (model.getRowData().getCell(0).isEmpty())

每次都必须包含在 try/catch 中会很痛苦。

通知用户:在您可以想象向最终用户呈现有用的错误消息的情况下,应使用已检查的异常。这就是“当它发生时你会怎么做?”我在上面提出的问题。它也与第 1 项有关。由于您可以预测客户端 API 系统之外的某些东西可能会导致文件不存在,因此您可以合理地告诉用户:“错误:找不到文件 'goodluckfindingthisfile'”由于您的非法行号是由内部错误引起的,并且不是用户的过错,因此您实际上没有任何有用的信息可以提供给他们。如果您的应用程序不允许运行时异常进入控制台,它可能最终会给它们一些丑陋的消息,例如:“发生内部错误:IllegalArgumentException in ....”简而言之,如果您不认为您的客户端程序员可以以帮助用户的方式解释您的异常,那么您可能不应该使用已检查的异常。

所以这些是我的规则。有点做作,毫无疑问会有例外(如果你愿意,请帮助我完善它们)。但我的主要论点是,在某些情况下,例如 FileNotFoundException,已检查异常在 API 协定中与参数类型一样重要且有用。所以我们不应该仅仅因为它被滥用就放弃它。

抱歉,我不是故意让这件事变得如此冗长和胡扯。最后让我提出两个建议:

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

B:客户端程序员:养成在开发早期创建包装异常(google it)的习惯。 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) */}

评论不用于扩展讨论;此对话已moved to chat
a
aioobe

关于检查异常的事情是,按照通常对概念的理解,它们并不是真正的异常。相反,它们是 API 替代返回值。

异常的整个想法是,在调用链的某处抛出的错误可以冒泡并由更远的代码处理,而无需干预代码担心它。另一方面,已检查的异常要求抛出者和捕获者之间的每一级代码都声明他们知道可以通过它们的所有形式的异常。这在实践中与如果检查的异常只是调用者必须检查的特殊返回值几乎没有什么不同。例如。[伪代码]:

public [int or IOException] writeToStream(OutputStream stream) {
    [void or IOException] a= stream.write(mybytes);
    if (a instanceof IOException)
        return a;
    return mybytes.length;
}

由于 Java 不能做替代返回值,或者简单的内联元组作为返回值,所以检查异常是一个合理的响应。

问题是很多代码,包括大量的标准库,滥用检查的异常来处理真正的异常情况,你可能很想赶上几个级别。为什么 IOException 不是 RuntimeException?在所有其他语言中,我都可以让 IO 异常发生,如果我不采取任何措施来处理它,我的应用程序将停止,并且我会得到一个方便的堆栈跟踪来查看。这是可能发生的最好的事情。

可能有两个方法,你想从整个写入到流的过程中捕获所有 IOExceptions,中止该过程并跳转到错误报告代码;在 Java 中,如果不在每个调用级别添加“抛出 IOException”,即使是本身不执行 IO 的级别,也无法做到这一点。这样的方法不需要知道异常处理;必须为其签名添加例外:

不必要地增加耦合;使界面签名很难更改;降低代码的可读性;太烦人了,程序员的常见反应是通过做一些可怕的事情来打败系统,比如“抛出异常”、“捕获(异常 e){}”,或者将所有内容包装在 RuntimeException 中(这使得调试更加困难)。

然后有很多荒谬的库异常,例如:

try {
    httpconn.setRequestMethod("POST");
} catch (ProtocolException e) {
    throw new CanNeverHappenException("oh dear!");
}

当你不得不用这样可笑的杂物来弄乱你的代码时,难怪受检异常会受到很多讨厌,即使这只是简单的糟糕的 API 设计。

另一个特别不好的影响是控制反转,其中组件 A 向通用组件 B 提供回调。组件 A 希望能够让异常从其回调中抛出回它调用组件 B 的地方,但它不能因为这会改变由 B 修复的回调接口。A 只能通过将真正的异常包装在 RuntimeException 中来做到这一点,这是要编写的更多异常处理样板。

在 Java 及其标准库中实现的已检查异常意味着样板、样板、样板。在已经冗长的语言中,这不是胜利。


在您的代码示例中,最好将异常链接起来,以便在阅读日志时可以找到原始原因: throw CanNeverHappenException(e);
@Mister:我的意思是,在 Java 中实现的已检查异常的行为实际上更像是 C 中的返回值,而不是我们可能从 C++ 和其他 Java 之前的语言中识别的传统“异常”。 IMO 这确实会导致混乱和糟糕的设计。
同意标准库滥用检查异常肯定会增加混乱和不良捕获行为。而且,通常它只是来自糟糕的文档,例如,当“发生其他一些 I/O 错误”时,像 disconnect() 这样的拆卸方法会抛出 IOException。嗯,我断网了!我是否正在泄漏句柄或其他资源?我需要重试吗?在不知道为什么会发生的情况下,我无法得出我应该采取的行动,所以我不得不猜测我是应该接受它、重试还是保释。
+1 用于“API 替代返回值”。查看检查异常的有趣方式。
我认为从概念上讲,将异常作为替代返回值的想法是有道理的,但我会更进一步。这是另一种返回机制。异常可以通过函数调用堆栈中的多个条目传递相同的值,从而无声地绕过进程中的大量代码。这不是普通的return机制能做到的,也是异常让我们能够实现解耦的原因。底线,例外是流控制,与陈词滥调相反。它们是一个更有限、更易于管理的(因为对状态有更大的保证)GOTO。
c
cletus

我不会针对检查的异常重新讨论所有(许多)原因,而是只选择一个。我已经记不清我写这段代码的次数了:

try {
  // do stuff
} catch (AnnoyingcheckedException e) {
  throw new RuntimeException(e);
}

99%的时间我对此无能为力。 finally 块进行任何必要的清理(或者至少它们应该)。

我也记不清我看到这个的次数了:

try {
  // do stuff
} catch (AnnoyingCheckedException e) {
  // do nothing
}

为什么?因为有人不得不处理它并且很懒惰。是不是错了?当然。它会发生吗?绝对地。如果这是一个未经检查的异常呢?该应用程序将刚刚死掉(这比吞下异常更可取)。

然后我们有令人愤怒的代码,它使用异常作为流控制的一种形式,就像 java.text.Format 一样。嗡嗡声。错误的。用户将“abc”放入表单的数字字段中也不例外。

好吧,我想这是三个原因。


但是如果异常被正确捕获,您可以通知用户,执行其他任务(记录?)并以受控方式退出应用程序。我同意某些 API 部分可以设计得更好。出于懒惰的程序员的原因,好吧,我认为作为一名程序员,你对你的代码负有 100% 的责任。
请注意,try-catch-rethrow 允许您指定消息 - 我通常使用它来添加有关状态变量内容的信息。一个常见的例子是 IOExceptions 添加相关文件的 absolutePathName()。
我认为 Eclipse 之类的 IDE 对您看到空的 catch 块的次数有很多责任。真的,他们应该默认重新抛出。
“99% 的时间我对此无能为力”——错了,您可以向用户显示“无法连接到服务器”或“IO 设备失败”的消息,而不是让应用程序崩溃由于一点网络故障。您的两个示例都是糟糕程序员的工作艺术。您应该攻击糟糕的程序员,而不是检查异常本身。就像我在用它作为沙拉酱时攻击胰岛素对治疗糖尿病没有帮助一样。
@YasmaniLlanes 你不能总是做这些事情。有时你需要遵守一个界面。当您设计良好的可维护 API 时尤其如此,因为您不能只是开始到处抛出副作用。这两者以及它所带来的复杂性都会大规模地严重影响您。所以是的,99% 的情况下,没有什么可做的。
B
Boann

我知道这是一个老问题,但我花了一段时间来处理检查的异常,我有一些东西要补充。请原谅我的长度!

我对检查异常的主要不满是它们破坏了多态性。让它们与多态接口很好地配合是不可能的。

采用良好的 Java List 接口。我们有常见的内存实现,例如 ArrayListLinkedList。我们还有骨架类 AbstractList,它可以很容易地设计新类型的列表。对于只读列表,我们只需要实现两个方法:size()get(int index)

此示例 WidgetList 类从文件中读取一些 Widget 类型的固定大小对象(未显示):

class WidgetList extends AbstractList<Widget> {
    private static final int SIZE_OF_WIDGET = 100;
    private final RandomAccessFile file;

    public WidgetList(RandomAccessFile file) {
        this.file = file;
    }

    @Override
    public int size() {
        return (int)(file.length() / SIZE_OF_WIDGET);
    }

    @Override
    public Widget get(int index) {
        file.seek((long)index * SIZE_OF_WIDGET);
        byte[] data = new byte[SIZE_OF_WIDGET];
        file.read(data);
        return new Widget(data);
    }
}

通过使用熟悉的 List 界面公开小部件,您可以检索项目 (list.get(123)) 或迭代列表 (for (Widget w : list) ...),而无需了解 WidgetList 本身。可以将此列表传递给任何使用通用列表的标准方法,或将其包装在 Collections.synchronizedList 中。使用它的代码既不需要知道也不需要关心“小部件”是现场制作的,来自数组,还是从文件、数据库、网络或未来的子空间中继中读取的。它仍然可以正常工作,因为 List 接口已正确实现。

除非它不是。上面的类无法编译,因为文件访问方法可能会抛出 IOException,这是一个您必须“捕获或指定”的检查异常。您不能将其指定为抛出——编译器不会让您这样做,因为这会违反 List 接口的约定。并且 WidgetList 本身没有任何有用的方法可以处理异常(稍后我将详细说明)。

显然,唯一要做的就是捕获并重新抛出已检查的异常作为一些未检查的异常:

@Override
public int size() {
    try {
        return (int)(file.length() / SIZE_OF_WIDGET);
    } catch (IOException e) {
        throw new WidgetListException(e);
    }
}

public static class WidgetListException extends RuntimeException {
    public WidgetListException(Throwable cause) {
        super(cause);
    }
}

((编辑:Java 8 为这种情况添加了一个 UncheckedIOException 类:用于跨多态方法边界捕获和重新抛出 IOException。有点证明了我的观点!))

所以检查异常在这种情况下根本不起作用。你不能扔它们。同样适用于由数据库支持的智能 Map,或通过 COM 端口连接到量子熵源的 java.util.Random 实现。一旦您尝试对多态接口的实现做任何新颖的事情,检查异常的概念就会失败。但是检查的异常是如此阴险,它们仍然不会让你平静下来,因为你仍然必须从低级方法中捕获并重新抛出任何异常,从而使代码和堆栈跟踪变得混乱。

我发现无处不在的 Runnable 接口经常被退回到这个角落,如果它调用了引发检查异常的东西。它不能按原样抛出异常,所以它所能做的就是通过捕获并重新抛出 RuntimeException 来使代码混乱。

实际上,如果你使用 hack,你可以抛出未声明的检查异常。 JVM 在运行时并不关心检查异常规则,所以我们只需要欺骗编译器。最简单的方法是滥用泛型。这是我的方法(显示类名是因为(在 Java 8 之前)它在泛型方法的调用语法中是必需的):

class Util {
    /**
     * Throws any {@link Throwable} without needing to declare it in the
     * method's {@code throws} clause.
     * 
     * <p>When calling, it is suggested to prepend this method by the
     * {@code throw} keyword. This tells the compiler about the control flow,
     * about reachable and unreachable code. (For example, you don't need to
     * specify a method return value when throwing an exception.) To support
     * this, this method has a return type of {@link RuntimeException},
     * although it never returns anything.
     * 
     * @param t the {@code Throwable} to throw
     * @return nothing; this method never returns normally
     * @throws Throwable that was provided to the method
     * @throws NullPointerException if {@code t} is {@code null}
     */
    public static RuntimeException sneakyThrow(Throwable t) {
        return Util.<RuntimeException>sneakyThrow1(t);
    }

    @SuppressWarnings("unchecked")
    private static <T extends Throwable> RuntimeException sneakyThrow1(
            Throwable t) throws T {
        throw (T)t;
    }
}

欢呼!使用它,我们可以在堆栈的任何深度抛出已检查异常,而无需声明它,无需将其包装在 RuntimeException 中,也不会弄乱堆栈跟踪!再次使用“WidgetList”示例:

@Override
public int size() {
    try {
        return (int)(file.length() / SIZE_OF_WIDGET);
    } catch (IOException e) {
        throw sneakyThrow(e);
    }
}

不幸的是,受检查异常的最后一个侮辱是编译器拒绝允许您捕获受检查异常,如果它有缺陷地认为它不能被抛出。 (未经检查的异常没有这条规则。)为了捕捉偷偷抛出的异常,我们必须这样做:

try {
    ...
} catch (Throwable t) { // catch everything
    if (t instanceof IOException) {
        // handle it
        ...
    } else {
        // didn't want to catch this one; let it go
        throw t;
    }
}

这有点尴尬,但从好的方面来说,它仍然比用于提取包装在 RuntimeException 中的已检查异常的代码稍微简单一些。

幸运的是,throw t; 语句在这里是合法的,即使检查了 t 的类型,这要感谢 Java 7 中添加的关于重新抛出捕获的异常的规则。

当检查的异常遇到多态性时,相反的情况也是一个问题:当一个方法被指定为可能抛出一个检查的异常,但重写的实现没有。例如,抽象类 OutputStreamwrite 方法都指定 throws IOExceptionByteArrayOutputStream 是写入内存数组而不是真正的 I/O 源的子类。它被覆盖的 write 方法不会导致 IOException,因此它们没有 throws 子句,您可以调用它们而不必担心 catch-or-specify 要求。

除了并非总是如此。假设 Widget 具有将其保存到流中的方法:

public void writeTo(OutputStream out) throws IOException;

声明这个方法接受一个普通的 OutputStream 是正确的做法,因此它可以多态地用于各种输出:文件、数据库、网络等等。和内存数组。但是,对于内存中的数组,有一个虚假的要求来处理实际上不可能发生的异常:

ByteArrayOutputStream out = new ByteArrayOutputStream();
try {
    someWidget.writeTo(out);
} catch (IOException e) {
    // can't happen (although we shouldn't ignore it if it does)
    throw new RuntimeException(e);
}

像往常一样,检查的异常会阻碍。如果您的变量被声明为具有更多开放式异常要求的基类型,则您必须为这些异常添加处理程序,即使您知道它们不会在您的应用程序中发生。

但是等等,检查的异常实际上是非常烦人的,它们甚至不允许你做相反的事情!想象一下你当前捕获了由 write 调用抛出的任何 IOExceptionOutputStream 上,但您想将变量的声明类型更改为 ByteArrayOutputStream,编译器会责备您试图捕获它说不能抛出的已检查异常。

该规则会导致一些荒谬的问题。例如,OutputStream 的三个 write 方法之一ByteArrayOutputStream 覆盖。具体来说,write(byte[] data) 是一种便捷方法,它通过调用 write(byte[] data, int offset, int length) 以偏移量为 0 和数组长度来写入完整数组。 ByteArrayOutputStream 覆盖三参数方法,但按原样继承一参数便捷方法。继承的方法完全正确,但它包含一个不需要的 throws 子句。这可能是 ByteArrayOutputStream 设计中的一个疏忽,但他们永远无法修复它,因为它会破坏与捕获异常的任何代码的源代码兼容性——从未、永远不会、永远不会抛出异常!

该规则在编辑和调试期间也很烦人。例如,有时我会临时注释掉一个方法调用,如果它可能抛出一个检查异常,编译器现在会抱怨本地 trycatch 块的存在。所以我也必须将它们注释掉,现在在编辑其中的代码时,IDE 将缩进到错误的级别,因为 {} 被注释掉了。呸!这是一个小小的抱怨,但似乎检查异常所做的唯一一件事就是造成麻烦。

我快完成了。我对检查异常的最后一个挫折是,在大多数呼叫站点,您对它们没有任何用处。理想情况下,当出现问题时,我们会有一个称职的特定于应用程序的处理程序,可以通知用户问题和/或适当地结束或重试操作。只有堆栈上的处理程序才能做到这一点,因为它是唯一知道总体目标的处理程序。

相反,我们得到了以下惯用语,它作为一种关闭编译器的方式很猖獗:

try {
    ...
} catch (SomeStupidExceptionOmgWhoCares e) {
    e.printStackTrace();
}

在 GUI 或自动化程序中,不会看到打印的消息。更糟糕的是,它会在异常发生后继续处理其余代码。异常实际上不是错误吗?那就不要打印了。否则,一会儿其他东西会爆炸,到那时原始异常对象将消失。这个习语并不比 BASIC 的 On Error Resume Next 或 PHP 的 error_reporting(0); 好。

调用某种记录器类也好不到哪里去:

try {
    ...
} catch (SomethingWeird e) {
    logger.log(e);
}

这就像 e.printStackTrace(); 一样懒惰,并且仍然在不确定状态下继续使用代码。此外,特定日志系统或其他处理程序的选择是特定于应用程序的,因此这会损害代码重用。

可是等等!有一种简单而通用的方法可以找到特定于应用程序的处理程序。它在调用堆栈的上层(或设置为线程的 uncaught exception handler)。所以在大多数情况下,您需要做的就是将异常抛到更高的堆栈上。例如,throw e;。已检查的异常只会妨碍您。

我确信在设计语言时检查异常听起来是个好主意,但在实践中我发现它们都很麻烦而且没有任何好处。


对于您使用 WidgetList 的大小方法,我会将大小缓存在一个变量中并在构造函数中设置它。构造函数可以随意抛出异常。如果文件在使用 WidgetList 时发生更改,这将不起作用,如果这样做可能会很糟糕。
SomeStupidExceptionOmgWhoCares 很好,有人很关心扔掉它。因此,要么它永远不应该被抛出(糟糕的设计),要么你应该真正处理它。不幸的是,设计糟糕的 1.0 之前的类(字节数组输出流)的糟糕实现也是如此。
正确的习惯用法应该是一个指令,该指令将捕获由嵌套子例程调用抛出的任何指定异常,并将它们重新抛出并包裹在 RuntimeException 中。请注意,一个例程可以同时声明为 throws IOException 并且还指定从嵌套调用抛出的任何 IOException 都应被视为意外并包装。
我是一名专业的 C# 开发人员,有一些 Java 经验,偶然发现了这篇文章。我很困惑为什么有人会支持这种奇怪的行为。在 .NET 中,如果我想捕获特定类型的异常,我可以捕获它。如果我只想让它被扔到堆栈上,那就没什么可做的了。我希望 Java 不是那么古怪。 :)
关于“有时我会暂时注释掉一个方法调用” - 我学会了为此使用 if (false) 。它避免了 throw 子句问题,并且警告帮助我更快地导航回来。 +++ 也就是说,我同意你写的一切。已检查的异常有一些价值,但与它们的成本相比,这个价值可以忽略不计。他们几乎总是会碍事。
C
Carl Manaster

好吧,这与显示堆栈跟踪或静默崩溃无关。这是关于能够在层之间传达错误。

检查异常的问题是它们鼓励人们吞下重要的细节(即异常类)。如果您选择不接受这些细节,那么您必须在整个应用程序中不断添加 throws 声明。这意味着 1) 新的异常类型将影响大量函数签名,以及 2) 您可能会错过您实际想要捕获的异常的特定实例(例如,您为将数据写入文件。辅助文件是可选的,所以你可以忽略它的错误,但因为签名throws IOException,很容易忽略这一点)。

我现在实际上正在应用程序中处理这种情况。我们将几乎所有异常重新打包为 AppSpecificException。这使得签名非常干净,我们不必担心签名中的 throws 爆炸。

当然,现在我们需要专门化更高级别的错误处理,实现重试逻辑等。但是,一切都是 AppSpecificException,所以我们不能说“如果抛出 IOException,请重试”或“如果抛出 ClassNotFound,则完全中止”。我们没有可靠的方法来处理真正的异常,因为当它们在我们的代码和第三方代码之间传递时,它们会一次又一次地重新打包。

这就是为什么我非常喜欢 python 中的异常处理。您只能捕获您想要和/或可以处理的东西。其他所有东西都会冒泡,就好像你自己重新扔了一样(反正你已经做过了)。

我一次又一次地发现,在我提到的整个项目中,异常处理分为 3 类:

捕获并处理特定的异常。例如,这是为了实现重试逻辑。捕获并重新抛出其他异常。这里发生的一切通常是日志记录,它通常是一条陈词滥调的消息,例如“无法打开 $filename”。这些是您无能为力的错误;只有更高级别的人知道足以处理它。捕获所有内容并显示错误消息。这通常位于调度程序的根部,它所做的一切确保它可以通过非异常机制(弹出对话框、编组 RPC 错误对象等)将错误传达给调用者。


您可以创建 AppSpecificException 的特定子类以允许分离,同时保留普通方法签名。
对第 2 项的一个非常重要的补充是,它允许您向捕获的异常添加信息(例如,通过嵌套在 RuntimeException 中)。在堆栈跟踪中找不到文件的名称比隐藏在日志文件的深处要好得多。
基本上你的论点是“管理异常很累,所以我宁愿不处理它”。随着异常冒泡,它失去了意义,上下文制作实际上是无用的。作为 API 的设计者,你应该清楚地说明当出现问题时会发生什么,如果我的程序崩溃是因为我没有被告知这个或那个异常会“冒泡”,那么作为设计者的你,失败了由于您的失败,我的系统不够稳定。
这根本不是我要说的。你的最后一句话实际上同意我的看法。如果所有内容都包含在 AppSpecificException 中,那么它不会冒泡(并且含义/上下文丢失),并且,是的,API 客户端没有被通知 - 这正是检查异常发生的情况(就像它们在 java 中一样) ,因为人们不想处理具有大量 throws 声明的函数。
@Newtopian - 异常主要只能在“业务”或“请求”级别处理。以大粒度失败或重试是有意义的,而不是针对每一个微小的潜在失败。出于这个原因,异常处理的最佳实践被总结为“早扔,晚抓”。已检查的异常使得在正确级别管理可靠性变得更难,并鼓励大量错误编码的 catch 块。 literatejava.com/exceptions/…
a
aioobe

信噪比

首先,检查异常会降低代码的“信噪比”。 Anders Hejlsberg 还谈到了命令式编程与声明式编程,这是一个类似的概念。无论如何考虑以下代码片段:

从 Java 中的非 UI 线程更新 UI:

try {  
    // Run the update code on the Swing thread  
    SwingUtilities.invokeAndWait(() -> {  
        try {
            // Update UI value from the file system data  
            FileUtility f = new FileUtility();  
            uiComponent.setValue(f.readSomething());
        } catch (IOException e) {  
            throw new UncheckedIOException(e);
        }
    });
} catch (InterruptedException ex) {  
    throw new IllegalStateException("Interrupted updating UI", ex);  
} catch (InvocationTargetException ex) {
    throw new IllegalStateException("Invocation target exception updating UI", ex);
}

从 C# 中的非 UI 线程更新 UI:

private void UpdateValue()  
{  
   // Ensure the update happens on the UI thread  
   if (InvokeRequired)  
   {  
       Invoke(new MethodInvoker(UpdateValue));  
   }  
   else  
   {  
       // Update UI value from the file system data  
       FileUtility f = new FileUtility();  
       uiComponent.Value = f.ReadSomething();  
   }  
}  

这对我来说似乎更清楚。当你开始在 Swing 中做越来越多的 UI 工作时,检查异常开始变得非常烦人和无用。

越狱

即使是最基本的实现,例如 Java 的 List 接口,作为契约式设计工具的已检查异常也会失败。考虑一个由数据库或文件系统或任何其他引发检查异常的实现支持的列表。唯一可能的实现是捕获已检查的异常并将其作为未检查的异常重新抛出:

@Override
public void clear()  
{  
   try  
   {  
       backingImplementation.clear();  
   }  
   catch (CheckedBackingImplException ex)  
   {  
       throw new IllegalStateException("Error clearing underlying list.", ex);  
   }  
}  

现在你不得不问所有这些代码的意义是什么?检查的异常只是增加了噪音,异常已被捕获但未处理,并且合同设计(就检查的异常而言)已经崩溃。

结论

捕获异常与处理它们不同。

检查的异常会给代码增加噪音。

没有它们,异常处理在 C# 中运行良好。

我写了一篇关于这个 previously 的博客。


在示例中,Java 和 C# 都只是让异常传播而不处理它们(Java 通过 IllegalStateException)。不同之处在于您可能想要处理 FileNotFoundException,但处理 InvocationTargetException 或 InterruptedException 不太可能有用。
而在 C# 方式中,我怎么知道会发生 I/O 异常?此外,我永远不会从运行中抛出异常......我认为滥用异常处理。抱歉,但是对于您的那部分代码,我只能看到您的一面。
我们正在到达那里 :-) 因此,对于 API 的每个新版本,您都必须梳理所有调用并查找可能发生的任何新异常?公司内部的 API 很容易发生这种情况,因为它们不必担心向后兼容性。
你的意思是降低信噪比?
@TofuBeer 在底层API的接口改变后,不是被迫更新代码是件好事吗?如果您在那里只有未经检查的异常,那么您最终会在不知情的情况下得到一个损坏/不完整的程序。
L
Le Dude

Artima published an interview 与 .NET 的一位架构师 Anders Hejlsberg 一起,其中敏锐地涵盖了反对检查异常的论点。短暂的品尝者:

throws 子句,至少它在 Java 中实现的方式,不一定强制您处理异常,但如果您不处理它们,它会强制您准确确认哪些异常可能通过。它要求您要么捕获已声明的异常,要么将它们放入您自己的 throws 子句中。为了解决这个要求,人们会做一些荒谬的事情。例如,他们用“抛出异常”来装饰每个方法。这完全破坏了该功能,并且您只是让程序员编写了更多狼吞虎咽的东西。这对任何人都没有帮助。


我已经读过,对我来说,他的论点归结为“那里有糟糕的程序员”。
豆腐啤酒,一点也不。关键是很多时候你不知道如何处理被调用方法抛出的异常,甚至没有提到你真正感兴趣的情况。你打开一个文件,你得到一个 IO 异常,例如……这不是我的问题,所以我把它扔了。但是顶级调用方法只想停止处理并通知用户存在未知问题。检查的异常根本没有帮助。这是可能发生的一百万件奇怪的事情之一。
@yar,如果您不喜欢已检查的异常,请执行“抛出新的 RuntimeException(“我们在执行 Foo.bar() 时没想到这一点,e)”并完成它。
TofuBeer,我认为他真正的论点是那里有人类。总的来说,使用检查异常所带来的痛苦小于没有它们所带来的痛苦并不能令人信服。
@ThorbjørnRavnAndersen:不幸的是,.net 复制了 Java 的一个基本设计弱点,它使用异常的类型作为决定是否应该对其采取行动的主要手段,以及指示事物一般类型的主要手段那是错误的,而实际上这两个问题在很大程度上是正交的。重要的不是出了什么问题,而是状态对象处于什么状态。此外,默认情况下,.net 和 Java 都假定处理和解决异常通常是同一件事,而实际上它们通常是不同的。
t
tsimon

我最初同意你的观点,因为我一直支持检查异常,并开始思考为什么我不喜欢在 .Net 中没有检查异常。但后来我意识到我实际上并不喜欢检查异常。

回答你的问题,是的,我喜欢我的程序显示堆栈跟踪,最好是非常丑陋的。我希望应用程序爆炸成一堆你可能想看到的最丑陋的错误消息。

原因是,如果它这样做,我必须修复它,而且我必须立即修复它。我想立即知道有问题。

您实际处理了多少次异常?我不是在谈论捕获异常——我是在谈论处理它们?编写以下内容太容易了:

try {
  thirdPartyMethod();
} catch(TPException e) {
  // this should never happen
}

而且我知道您可以说这是不好的做法,并且“答案”是对异常做某事(让我猜猜,记录它?),但是在现实世界(tm)中,大多数程序员只是不这样做它。

所以,是的,如果我不必这样做,我不想捕获异常,并且我希望我的程序在我搞砸时会爆炸。默默地失败是最糟糕的结果。


Java 鼓励您做这种事情,这样您就不必为每个方法签名添加每种类型的异常。
有趣..自从我正确地接受了检查异常并适当地使用它们以来,我的程序就停止了在你面前客户不满的巨大蒸汽堆中爆炸。如果在开发过程中你有大而丑陋的堆栈跟踪,那么客户也一定会得到它们。当他看到 ArrayIndexOutOfBoundsException 在他崩溃的系统上具有一英里高的堆栈跟踪而不是一个小托盘通知说无法解析按钮 XYZ 的颜色配置时,我喜欢看到他的脸,因此使用默认值代替软件愉快地嗡嗡作响沿着
也许 Java 需要的是一个“cantHandle”声明,该声明将指定方法或 try/catch 代码块不准备处理其中发生的特定异常,并且任何通过显式以外的方式发生的此类异常在该方法中的 throw(与被调用的方法相反)应自动包装并在 RuntimeException 中重新抛出。恕我直言,检查的异常应该很少在没有被包装的情况下向上传播调用堆栈。
@Newtopian——我编写服务器和高可靠性软件并且已经这样做了 25 年。我的程序从未崩溃过,我使用的是高可用性、重试和重新连接、基于集成的金融和军事系统。我有绝对的客观基础来更喜欢运行时异常。受检查的异常使遵循正确的“早扔,晚抓”的最佳实践变得更加困难。正确的可靠性和错误处理处于“业务”、“连接”或“请求”级别。 (或者偶尔在解析数据时)。已检查的异常会妨碍正确执行。
您在这里谈论的例外是RuntimeExceptions,您确实不必捕获它,我同意您应该让程序从那里炸毁。您应该始终捕获和处理的异常是已检查的异常,例如 IOException。如果您得到一个 IOException,那么您的代码中没有什么需要修复的;您的程序不应该仅仅因为出现网络故障而崩溃。
C
Community

文章 Effective Java Exceptions 很好地解释了何时使用未检查以及何时使用检查异常。以下是该文章中的一些引述,以突出要点:

偶然性:一种预期的条件,要求一种方法的替代响应,可以用该方法的预期目的来表达。方法的调用者期望这些条件,并有应对它们的策略。故障:阻止方法实现其预期目的的计划外情况,如果不参考方法的内部实现就无法描述。

(SO 不允许使用表格,因此您可能需要阅读 original page...中的以下内容...)

偶然性 被认为是:设计的一部分 预期会发生:经常但很少发生 谁在乎它:调用方法的上游代码 示例:替代返回模式 最佳映射:已检查异常 错误被认为是:讨厌惊喜 预期发生:从不 谁在乎:需要解决问题的人 示例:编程错误、硬件故障、配置错误、丢失文件、不可用的服务器 最佳映射:未经检查的异常


我知道什么时候使用它们,我想知道为什么不遵循该建议的人...不要遵循该建议:-)
什么是编程错误以及如何将它们与使用错误区分开来?如果用户将错误的参数传递给程序,这是一个编程错误吗?从 Java 的角度来看,它可能不是编程错误,但从 shell 脚本的角度来看,它是一个编程错误。那么 args[] 中的无效参数是什么?它们是偶然事件还是错误?
@TofuBeer——因为Java库设计者选择将各种不可恢复的低级故障作为已检查异常,而它们显然应该不被检查。例如,FileNotFound 是唯一应该检查的 IOException。关于 JDBC——仅连接到数据库,可以合理地认为是一种意外情况。所有其他 SQLExceptions 都应该是失败的并且未经检查。错误处理应该正确地处于“业务”或“请求”级别——请参阅“早扔,晚抓”最佳实践。检查的异常是一个障碍。
你的论点有一个巨大的缺陷。 “意外”不能通过异常处理,而是通过业务代码和方法返回值来处理。正如这个词所说,例外是针对例外情况,因此是错误。
@MatteoMosca 错误返回码往往会被忽略,这足以取消它们的资格。实际上,任何不寻常的事情通常只能在堆栈中的某个位置处理,这是异常的用例。我可以想象像 File#openInputStream 返回 Either<InputStream, Problem> 之类的东西 - 如果这就是您的意思,那么我们可能会同意。
M
Mario Ortegón

在过去三年中,我一直在与几位开发人员合作开发相对复杂的应用程序。我们有一个代码库,它经常使用检查异常并进行适当的错误处理,而其他一些则没有。

到目前为止,我发现使用 Checked Exceptions 更容易使用代码库。当我使用其他人的 API 时,很高兴我可以准确地看到当我调用代码并通过记录、显示或忽略正确处理它们时会出现什么样的错误情况(是的,有忽略的有效案例异常,例如 ClassLoader 实现)。这使我正在编写的代码有机会恢复。我会传播所有运行时异常,直到它们被缓存并使用一些通用错误处理代码进行处理。当我发现一个我真的不想在特定级别处理的检查异常,或者我认为是编程逻辑错误时,我将它包装到 RuntimeException 中并让它冒泡。永远不要在没有充分理由的情况下吞下异常(这样做的充分理由相当稀缺)

当我使用没有检查异常的代码库时,我很难事先知道调用函数时我能期待什么,这可能会严重破坏一些东西。

这当然是一个偏好和开发人员技能的问题。编程和错误处理的两种方式都可以同样有效(或无效),所以我不会说有 The One Way。

总而言之,我发现使用 Checked Exceptions 更容易,特别是在有很多开发人员的大型项目中。


我愿意。对我来说,它们是合同的重要组成部分。无需详细了解 API 文档,我就可以快速了解最可能出现的错误情况。
同意。当我尝试进行网络调用时,我曾经体验过在 .Net 中检查异常的必要性。知道网络故障随时可能发生,我必须通读 API 的整个文档,以找出我需要专门针对该场景捕获的异常。如果 C# 已检查异常,我会立即知道。其他 C# 开发人员可能只会让应用程序因简单的网络错误而崩溃。
D
David Lichteblau

简而言之:

异常是一个 API 设计问题。 ——不多也不少。

检查异常的参数:

要理解为什么受检异常可能不是好事,让我们转过头来问:受检异常何时或为什么具有吸引力,即为什么您希望编译器强制声明异常?

答案很明显:有时您需要捕获异常,而这只有在被调用的代码为您感兴趣的错误提供特定异常类时才有可能。

因此,检查异常的论点是编译器强制程序员声明抛出哪些异常,并希望程序员随后也会记录特定的异常类和导致它们的错误。

但实际上,包 com.acme 通常只抛出 AcmeException 而不是特定的子类。然后调用者需要处理、声明或重新发送 AcmeExceptions,但仍不能确定是 AcmeFileNotFoundError 发生还是 AcmePermissionDeniedError

因此,如果您只对 AcmeFileNotFoundError 感兴趣,解决方案是向 ACME 程序员提出功能请求,并告诉他们实现、声明和记录 AcmeException 的子类。

那么为什么要打扰呢?

因此,即使有检查异常,编译器也不能强制程序员抛出有用的异常。这仍然只是 API 质量的问题。

因此,没有检查异常的语言通常不会差很多。程序员可能会倾向于抛出通用 Error 类而不是 AcmeException 的非特定实例,但如果他们完全关心 API 质量,他们终究会学会引入 AcmeFileNotFoundError

总的来说,异常的规范和文档与普通方法的规范和文档没有太大区别。这些也是 API 设计问题,如果程序员忘记实现或导出有用的功能,则需要改进 API,以便您可以有效地使用它。

如果您遵循这种推理,很明显,在 Java 等语言中很常见的声明、捕获和重新抛出异常的“麻烦”通常不会增加任何价值。

还值得注意的是,Java VM 没有检查异常——只有 Java 编译器检查它们,并且具有更改的异常声明的类文件在运行时是兼容的。 Java VM 的安全性并没有通过检查异常提高,只有编码风格。


你的论点自相矛盾。如果“有时您需要捕获异常”并且 API 质量通常很差,如果没有检查异常,您将不知道设计者是否忽略了记录某个方法引发了需要捕获的异常。将它与抛出 AcmeException 而不是 AcmeFileNotFoundError 相结合,祝你好运,找出你做错了什么以及你需要在哪里抓住它。已检查的异常为程序员提供了一些针对不良 API 设计的保护。
Java 库设计犯了严重的错误。 “已检查的异常”用于可预测的和可恢复的意外事件——例如找不到文件、连接失败。它们从不意味着或不适合低水平的系统性故障。强制打开要检查的文件会很好,但是对于无法写入单个字节/执行 SQL 查询等没有明智的重试或恢复。重试或恢复在“业务”或“请求”中正确处理" 级别,检查异常会变得毫无意义。 literatejava.com/exceptions/…
D
Daniel A.A. Pelsmaeker

例外类别

在谈论异常时,我总是参考 Eric Lippert's Vexing exceptions 博客文章。他将例外分为以下几类:

致命的——这些异常不是你的错:你无法阻止,也无法明智地处理它们。例如,OutOfMemoryError 或 ThreadAbortException。

Boneheaded - 这些异常是你的错:你应该阻止它们,它们代表代码中的错误。例如,ArrayIndexOutOfBoundsException、NullPointerException 或任何 IllegalArgumentException。

烦恼 - 这些异常并非异常,不是你的错,你无法阻止它们,但你必须处理它们。它们通常是不幸的设计决策的结果,例如从 Integer.parseInt 抛出 NumberFormatException 而不是提供在解析失败时返回布尔值 false 的 Integer.tryParseInt 方法。

外生的——这些异常通常是异常的,不是你的错,你不能(合理地)阻止它们,但你必须处理它们。例如,FileNotFoundException。

API 用户:

不得处理致命或愚蠢的异常。

应该处理令人烦恼的异常,但它们不应该出现在理想的 API 中。

必须处理外生异常。

已检查的异常

API 用户必须处理特定异常这一事实是调用者和被调用者之间方法契约的一部分。除其他事项外,合同还指定:被调用者期望的参数的数量和类型、调用者可以期望的返回值类型以及调用者期望处理的异常。

由于 API 中不应该存在令人烦恼的异常,因此只有这些外生异常必须经过检查异常才能成为方法契约的一部分。相对较少的异常是外生的,因此任何 API 都应该有相对较少的检查异常。

已检查异常是必须处理的异常。处理异常可以像吞下它一样简单。那里!异常被处理。时期。如果开发人员想以这种方式处理它,那很好。但他不能忽视异常,并且已经被警告。

API 问题

但是任何检查了令人烦恼和致命异常的 API(例如 JCL)都会给 API 用户带来不必要的压力。此类异常必须处理,但要么异常非常普遍以至于它本来就不应该是异常,要么在处理时无能为力。这导致 Java 开发人员讨厌检查异常。

此外,许多 API 没有适当的异常类层次结构,导致各种非外生异常原因由单个检查异常类(例如 IOException)表示。这也导致 Java 开发人员讨厌检查异常。

结论

外生异常是那些不是您的错、无法避免且应该处理的异常。这些构成了可以抛出的所有异常的一小部分。 API 应该只检查外生异常,而未检查所有其他异常。这将制作更好的 API,减轻 API 用户的压力,从而减少捕获所有、吞下或重新抛出未经检查的异常的需要。

所以不要讨厌 Java 和它的检查异常。相反,讨厌过度使用检查异常的 API。


并通过没有层次结构来滥用它们。
FileNotFound 和建立 JDBC/网络连接是偶然事件,并且是正确的检查异常,因为这些是可预测的和(可能)可恢复的。大多数其他 IOExceptions、SQLExceptions、RemoteException 等都是不可预测和不可恢复的故障,应该是运行时异常。由于错误的 Java 库设计,我们都被这个错误混为一谈,现在主要使用 Spring 和 Hibernate(他们的设计是正确的)。
您通常应该处理愚蠢的异常,尽管您可能不想将其称为“处理”。例如,在 Web 服务器中,我记录它们并向用户显示 500。由于异常是出乎意料的,所以在修复错误之前我能做的就差不多了。
N
Newtopian

好的......检查的异常并不理想并且有一些警告,但它们确实有目的。创建 API 时,会出现特定的失败案例,这些案例与该 API 的约定有关。在 Java 等强静态类型语言的上下文中,如果不使用检查异常,则必须依靠临时文档和约定来传达错误的可能性。这样做会消除编译器在处理错误方面可能带来的所有好处,并且您完全取决于程序员的善意。

那么,移除 Checked 异常,例如在 C# 中完成的,那么如何以编程和结构方式传达错误的可能性呢?如何通知客户端代码可能会发生这样那样的错误并且必须处理?

在处理受检异常时,我听到了各种各样的恐惧,它们被滥用,这是肯定的,但未经检查的异常也是如此。我说等几年,当 API 堆叠很多层时,你会乞求某种结构化手段的回归来传达失败。

以 API 层底部某处抛出异常并冒泡的情况为例,因为没有人知道甚至可能发生此错误,尽管这是一种在调用代码时非常合理的错误扔掉它(例如 FileNotFoundException 而不是 VogonsTrashingEarthExcept... 在这种情况下,我们是否处理它并不重要,因为没有任何东西可以处理它)。

许多人争辩说,无法加载文件几乎总是这个过程的世界末日,它必须死于可怕而痛苦的死亡。所以是的......当然......好吧......你为某事构建一个API并在某个时候加载文件......我作为所述API的用户只能响应......“你到底是谁来决定我的程序应该崩溃!”当然 考虑到异常被吞噬并且不留痕迹的选择,或者 EletroFlabbingChunkFluxManifoldChuggingException 的堆栈跟踪比 Marianna 沟槽更深,我会毫不犹豫地采用后者,但这是否意味着这是处理异常的理想方式?难道我们不能在中间的某个地方,每次遍历到一个新的抽象级别时都会重新转换和包装异常,以便它实际上意味着什么?

最后,我看到的大多数论点是“我不想处理异常,很多人不想处理异常。受检异常迫使我处理它们,因此我讨厌受检异常” 完全消除这种机制并将其降级为goto hell的鸿沟只是愚蠢的,缺乏判断力和远见。

如果我们消除检查异常,我们还可以消除函数的返回类型并始终返回一个“anytype”变量......这会让生活变得如此简单,不是吗?


如果有一种声明性的方式来说明块中的任何方法调用都不会抛出一些(或任何)检查异常,并且任何此类异常都应该自动包装并重新抛出,则检查异常将很有用。如果对声明为抛出检查异常的方法的调用以调用速度/返回换取异常处理速度(这样预期的异常几乎可以像正常程序流一样快地处理),它们可能会更有用。不过,目前这两种情况都不适用。
B
Boann

这不是反对检查异常的纯粹概念的论据,但 Java 用于它们的类层次结构是一个畸形秀。我们总是把事情简单地称为“异常”——这是正确的,因为语言规范 calls them that too——但是异常在类型系统中是如何命名和表示的呢?

通过类 Exception 可以想象吗?不,因为 Exception 是例外,同样例外是 Exception,除了那些 Exception 的例外,因为其他例外是实际上是 Error,这是另一种异常,一种额外异常的异常,除非它确实发生,否则永远不会发生,除非有时你必须这样做,否则你永远不应该捕获它。除了这还不是全部,因为您还可以定义其他既不是 Exception 也不是 Error 而只是 Throwable 异常的异常。

其中哪些是“已检查”异常? Throwable 是已检查异常,除非它们也是 Error,它们是未检查异常,然后是 Exception,它们也是 Throwable 并且是检查异常的主要类型,除了也有一个例外,如果它们也是 RuntimeException,因为这是另一种未经检查的异常。

RuntimeException 有什么用?就像名字所暗示的那样,它们是异常,就像所有 Exception 一样,它们发生在运行时,就像实际上所有异常一样,除了 RuntimeException 与其他运行时 Exception 相比是异常的因为除非您犯了一些愚蠢的错误,否则它们不应该发生,尽管 RuntimeException 绝不是 Error,因此它们适用于异常错误但实际上不是 Error 的事情。除了 RuntimeErrorException,它实际上是 ErrorRuntimeException。但是,不是所有的例外都应该代表错误的情况吗?是的,所有这些。除了 ThreadDeath,这是一个非常普通的例外,因为文档解释说这是“正常发生”,这就是他们将其设为 Error 类型的原因。

无论如何,由于我们将所有异常从中间分为 Error(用于异常执行异常,因此未检查)和 Exception(用于较少异常的执行错误,因此检查,除非它们不是),我们现在需要两种不同的几种异常。所以我们需要 IllegalAccessErrorIllegalAccessExceptionInstantiationErrorInstantiationExceptionNoSuchFieldErrorNoSuchFieldExceptionNoSuchMethodErrorNoSuchMethodExceptionZipErrorZipException

除了即使检查了异常,也总是有(相当简单的)方法可以欺骗编译器并在不检查的情况下将其抛出。如果您这样做,您可能会得到一个 UndeclaredThrowableException,除非在其他情况下,它可能会作为 UnexpectedExceptionUnknownException(与 UnknownError 无关,仅用于“严重异常")、ExecutionExceptionInvocationTargetExceptionExceptionInInitializerError

哦,我们不能忘记 Java 8 新奇的 UncheckedIOException,它是一个 RuntimeException 异常,旨在让您通过包装由 I/O 错误引起的已检查 IOException 异常(它不要导致 IOError 异常,尽管它也存在)异常难以处理,因此您需要不检查它们。

谢谢爪哇!


据我所知,这个答案只是以一种充满讽刺意味、可以说很有趣的方式说“Java 的异常是一团糟”。它似乎没有做的是解释为什么程序员倾向于避免试图理解这些事情应该如何工作。此外,在现实生活中(至少我有机会处理的情况),如果程序员不故意让他们的生活变得更艰难,那么异常远没有你描述的那么复杂。
P
Piotr Sobczyk

This article 是我读过的关于 Java 异常处理的最佳文章。

它支持未经检查的异常而不是检查的异常,但这种选择被非常彻底地解释并基于强有力的论据。

我不想在这里引用太多的文章内容(最好将它作为一个整体来阅读),但它涵盖了来自该线程的 unchecked exceptions 倡导者的大部分论点。特别是这个论点(这似乎很流行)被涵盖:

以 API 层底部某处抛出异常并冒泡的情况为例,因为没有人知道甚至可能发生此错误,尽管这是一种在调用代码时非常合理的错误扔掉它(例如 FileNotFoundException 而不是 VogonsTrashingEarthExcept... 在这种情况下,我们是否处理它并不重要,因为没有任何东西可以处理它)。

作者“回应”:

假设不应捕获所有运行时异常并允许传播到应用程序的“顶部”,这是绝对错误的。 (...) 对于每个需要明确处理的异常情况 - 由系统/业务需求 - 程序员必须决定在哪里捕获它以及在捕获条件后做什么。这必须严格根据应用程序的实际需求来完成,而不是基于编译器警报。必须允许所有其他错误自由传播到将记录它们的最顶层处理程序,并且将采取优雅(可能是终止)操作。

主要思想或文章是:

当谈到软件中的错误处理时,唯一安全和正确的假设是,绝对存在的每个子程序或模块都可能发生故障!

因此,如果“没有人知道甚至可能发生此错误”,则该项目有问题。正如作者所建议的那样,此类异常应至少由最通用的异常处理程序(例如,处理所有未由更具体处理程序处理的异常的处理程序)处理。

可悲的是,似乎没有多少人发现这篇很棒的文章:-(。我衷心建议所有犹豫哪种方法更好的人花点时间阅读它。


E
Eponymous

事实上,检查异常一方面增加了程序的健壮性和正确性(您被迫对接口进行正确的声明——方法抛出的异常基本上是一种特殊的返回类型)。另一方面,您面临的问题是,由于异常“冒泡”,因此当您更改异常时,您通常需要更改大量方法(所有调用者,以及调用者的调用者等)方法抛出。

Java 中的检查异常并不能解决后一个问题; C# 和 VB.NET 把婴儿和洗澡水一起扔掉。

this OOPSLA 2005 paper(或 related technical report)中描述了一种采取中间道路的好方法。)

简而言之,它允许你说:method g(x) throws like f(x),这意味着 g 抛出了 f 抛出的所有异常。瞧,检查了没有级联更改问题的异常。

尽管这是一篇学术论文,但我鼓励您阅读(部分)它,因为它很好地解释了检查异常的好处和坏处。


C
Community

问题

我看到的异常处理机制最糟糕的问题是它引入了大规模的代码重复!老实说:在大多数项目中,95% 的时间里,开发人员真正需要做的就是以某种方式将异常传达给用户(在某些情况下,也传达给开发团队,例如通过发送一个 e -带有堆栈跟踪的邮件)。因此,通常在处理异常的每个地方都使用相同的代码行/代码块。

假设我们在每个 catch 块中对某种类型的检查异常进行简单的日志记录:

try{
   methodDeclaringCheckedException();
}catch(CheckedException e){
   logger.error(e);
}

如果这是一个常见的例外,在更大的代码库中甚至可能有数百个这样的 try-catch 块。现在让我们假设我们需要引入基于弹出对话框的异常处理而不是控制台日志记录,或者开始另外向开发团队发送电子邮件。

等一下……我们真的要编辑代码中的数百个位置吗?!你明白我的意思:-)。

解决方案

我们为解决该问题所做的工作是将异常处理程序(我将进一步称为EH)的概念引入集中异常处理。对于需要处理异常的每个类,我们的 Dependency Injection 框架都会注入一个异常处理程序实例。所以异常处理的典型模式现在看起来像这样:

try{
    methodDeclaringCheckedException();
}catch(CheckedException e){
    exceptionHandler.handleError(e);
}

现在要自定义我们的异常处理,我们只需要在一个地方更改代码(EH 代码)。

当然,对于更复杂的情况,我们可以实现 EH 的几个子类并利用 DI 框架提供给我们的特性。通过更改我们的 DI 框架配置,我们可以轻松地全局切换 EH 实现或将 EH 的特定实现提供给具有特殊异常处理需求的类(例如使用 Guice @Named 注释)。

这样,我们可以毫不费力地区分开发和发布版本的应用程序中的异常处理行为(例如,开发 - 记录错误并暂停应用程序,生产 - 记录错误并提供更多详细信息并让应用程序继续执行)。

最后一件事

最后但并非最不重要的一点是,似乎只需将我们的异常“向上”传递,直到它们到达某个顶级异常处理类,就可以获得相同类型的集中化。但这会导致我们的方法的代码和签名混乱,并引入该线程中其他人提到的维护问题。


发明异常是为了对它们做一些有用的事情。将它们写入日志文件或渲染漂亮的窗口是没有用的,因为原来的问题并没有得到解决。做一些有用的事情需要尝试不同的解决方案策略。示例:如果我无法从服务器 AI 获取我的数据,请在服务器 B 上尝试它。或者如果算法 A 产生堆溢出,我尝试算法 B,它要慢得多但可能会成功。
@ceving 是的,这在理论上都是好的和真实的。但是现在让我们回到练习单词。请诚实地回答你在实际项目中多久做一次?在这个实际项目中,catch 块的哪一部分对异常做了真正“有用”的事情? 10%就好了。产生异常的常见问题就像试图从不存在的文件中读取配置、OutOfMemoryErrors、NullPointerExceptions、数据库约束完整性错误等。你真的试图从所有这些错误中恢复吗?我不相信你:)。通常没有办法恢复。
@PiotrSobczyk:如果程序由于 suer 请求而采取了一些行动,并且操作以某种方式失败,并且没有损坏系统状态中的任何内容,那么通知用户操作无法完成是非常有用的处理情况的方法。 C# 和 .net 中异常的最大失败是没有一致的方法来确定系统状态中的任何内容是否可能已损坏。
正确,@PiotrSobczyk。大多数情况下,为响应异常而采取的唯一正确操作是回滚事务并返回错误响应。 “解决异常”的想法意味着我们没有(也不应该)拥有的知识和权威,并且违反了封装。如果我们的应用程序不是数据库,我们不应该尝试修复数据库。彻底失败并避免写入错误数据,ceving 已经足够有用了。
@PiotrSobczyk 昨天,我处理了一个“无法读取对象”异常(这只会发生,因为底层数据库已经在软件之前更新 - 这永远不会发生,但由于人为错误有可能)通过故障转移到数据库的历史版本保证指向对象的旧版本。
V
Vlasec

没有人提到的一件重要的事情是它如何干扰接口和 lambda 表达式。

假设您定义了一个 MyAppException extends Exception。它是您的应用程序抛出的所有异常所继承的顶级异常。在某些地方您不想对特定异常做出反应,您希望调用者解决它,因此您声明 throws MyAppException

在您想使用其他人的界面之前,一切看起来都很好。显然他们没有声明要抛出 MyAppException 的意图,因此编译器甚至不允许您调用在其中声明 throws MyAppException 的方法。这对于 java.util.function 来说尤其痛苦。

但是,如果您的异常扩展 RuntimeException,则接口将没有问题。如果您愿意,可以在 JavaDoc 中提及异常。但除此之外,它只是默默地冒出任何东西。当然,这意味着它可以终止您的应用程序。但是在许多企业软件中,您都有异常处理层,未经检查的异常可以节省很多麻烦。


但这正是检查异常的重点。如果接口没有指定检查异常,那么这个接口的用户可以确定不会抛出异常。这基本上意味着,不允许操作的执行失败。如果你的实现不能保证它,那么你不应该实现它。或者您应该仔细考虑如何处理失败,因为接口不允许您将其显式报告给调用者。
@DmitriiSemikin 关键是,Java 8 中出现了一些有趣的语法糖,如果有检查的异常,使用 PredicateFunction 等会变得很麻烦。人们更愿意在 lambda 之外处理异常,但检查异常的方式不允许这样做。如果 Predicate 在其 test 方法中抛出 Exception,那么它将适用于任何已检查的异常,但它总是需要嵌套在 try/catch-all 中,即使 lambda 根本没有抛出异常,这将比我们目前的情况更糟。
如果 lambda 从一开始就是语言的一个组成部分,而不是匿名类的某种语法糖,那么它们可能会有一些特殊的接口,其中抛出的检查异常会自动添加到它们的 throws 声明中,并且声明或捕获已检查异常的义务将转移到使用 lambda 的类。但由于情况并非如此,库通常更喜欢未经检查的异常,即使在检查异常本来是一个更好的主意的情况下,以保护语法糖。
我会说,我同意,lambdas 和检查异常不能很好地结合在一起。但可能也是有原因的。我发现让 Lambdas 抛出异常(检查或未检查)通常不是一个好主意。因为你永远不知道,这个 lambda 将在哪里使用(因此,你需要在哪里捕获异常......它甚至可以是其他线程)。因此,对于 Lambdas,我个人会放弃使用异常(如果可能的话)并使用其他报告错误的方式。事实上,检查的异常不能很好地与 lambda 一起工作,这只是突出了它。
问题在于 API 只接受不引发检查异常的 lambda。 Stream API 是最大的罪魁祸首。然而,完全有可能以这样一种方式编写 Stream API,即检查异常是可能的,尽管由于缺乏语言支持而有点麻烦。见这里:github.com/hjohn/MediaSystem-v2/blob/master/mediasystem-util/…
D
Dave Elton

要尝试仅解决未回答的问题:

如果你抛出 RuntimeException 子类而不是 Exception 子类,那么你怎么知道你应该捕获什么?

这个问题包含似是而非的推理恕我直言。仅仅因为 API 告诉您它抛出了什么并不意味着您在所有情况下都以相同的方式处理它。换句话说,您需要捕获的异常取决于您使用引发异常的组件的上下文。

例如:

如果我正在为数据库编写连接测试器,或者要检查用户输入 XPath 的有效性,那么我可能想要捕获并报告操作引发的所有已检查和未检查的异常。

但是,如果我正在编写一个处理引擎,我可能会以与 NPE 相同的方式处理 XPathException(已检查):我会让它运行到工作线程的顶部,跳过该批处理的其余部分,记录问题(或将其发送给支持部门进行诊断)并留下反馈以供用户联系支持。


确切地。简单直接,异常处理的方式。正如 Dave 所说,正确的异常处理通常是在高层次上完成的。 “早扔,晚抓”是原则。检查异常使这变得困难。
M
Martin

正如人们已经说过的那样,Java 字节码中不存在已检查的异常。它们只是一种编译器机制,与其他语法检查不同。我看到检查异常很像我看到编译器抱怨冗余条件:if(true) { a; } b;。这很有帮助,但我可能是故意这样做的,所以让我忽略你的警告。

事实是,如果你强制执行检查异常,你将无法强迫每个程序员“做正确的事”,而现在其他所有人都是附带损害,他们只是因为你制定的规则而讨厌你。

修复那里的不良程序!不要试图修复语言以不允许它们!对于大多数人来说,“对异常做一些事情”实际上只是告诉用户它。我也可以告诉用户一个未经检查的异常,所以不要让你的检查异常类离开我的 API。


是的,我只是想强调无法访问的代码(会产生错误)和具有可预测结果的条件之间的区别。我稍后会删除此评论。
C
Chuck Conway

Anders 在软件工程电台的 episode 97 中谈到了检查异常的缺陷以及他为什么将它们排除在 C# 之外。


J
Joshua

我在 c2.com 上的文章与原来的形式基本保持不变:CheckedExceptionsAreIncompatibleWithVisitorPattern

总之:

访问者模式及其亲属是一类接口,其中间接调用者和接口实现都知道异常,但接口和直接调用者形成一个不知道的库。

CheckedExceptions 的基本假设是所有声明的异常都可以从调用具有该声明的方法的任何点抛出。 VisitorPattern 揭示了这个假设是错误的。

在这种情况下,检查异常的最终结果是大量原本无用的代码,它们基本上在运行时删除了编译器的检查异常约束。

至于根本问题:

我的一般想法是顶级处理程序需要解释异常并显示适当的错误消息。我几乎总是看到 IO 异常、通信异常(出于某种原因 API 区分)或任务致命错误(程序错误或后备服务器上的严重问题),所以如果我们允许堆栈跟踪严重的问题,这应该不会太难服务器问题。


您应该在接口中有类似 DAGNodeException 的内容,然后捕获 IOException 并将其转换为 DAGNodeException: public void call(DAGNode arg) throws DAGNodeException;
@TofuBeer,这正是我的观点。我发现不断包装和展开异常比删除检查的异常更糟糕。
那么我们完全不同意......但是您的文章仍然没有回答真正的潜在问题,即当抛出运行时异常时如何阻止应用程序向用户显示堆栈跟踪。
@TofuBeer -- 当它失败时,告诉用户它失败是正确的!除了用“空”或不完整/不正确的数据“掩盖”失败之外,您还有什么选择?假装成功是谎言,只会让事情变得更糟。凭借 25 年的高可靠性系统经验,重试逻辑只能在适当的情况下谨慎使用。我还希望访客可能再次失败,无论您重试多少次。除非您正在驾驶飞机,否则切换到相同算法的第二个版本是不切实际且不可信的(并且可能无论如何都会失败)。
T
Thomas W

Checked exceptions 的原始形式是尝试处理意外事件而不是失败。值得称赞的目标是突出特定的可预测点(无法连接、找不到文件等)并确保开发人员处理了这些问题。

原始概念中从未包含的内容是强制宣布大量系统性和不可恢复的故障。这些失败永远不会正确地被声明为检查异常。

代码中通常可能出现故障,并且 EJB、Web 和 Swing/AWT 容器已经通过提供最外层的“失败请求”异常处理程序来满足这一点。最基本的正确策略是回滚事务并返回错误。

一个关键点是运行时和检查的异常在功能上是等效的。没有检查异常可以做的处理或恢复,运行时异常不能。

反对“已检查”异常的最大论据是大多数异常无法修复。简单的事实是,我们不拥有损坏的代码/子系统。我们看不到实现,我们不对它负责,也无法修复它。

如果我们的应用程序不是数据库......我们不应该尝试修复数据库。这将违反封装原则。

特别成问题的是 JDBC (SQLException) 和 RMI for EJB (RemoteException) 领域。不是根据最初的“检查异常”概念来识别可修复的意外事件,而是将这些强制普遍存在的、实际上不可修复的系统可靠性问题广泛声明。

Java 设计中的另一个严重缺陷是异常处理应该正确地置于最高可能的“业务”或“请求”级别。这里的原则是“早扔,晚赶”。已检查的异常几乎没有什么作用,但会阻碍这一点。

我们在 Java 中有一个明显的问题,即需要数千个什么都不做的 try-catch 块,其中很大一部分 (40%+) 被错误编码。这些几乎都没有实现任何真正的处理或可靠性,但会带来很大的编码开销。

最后,“检查异常”与 FP 函数式编程几乎不兼容。

他们坚持“立即处理”与“赶上”异常处理最佳实践以及任何抽象循环/或控制流的 FP 结构都不一致。

许多人谈论“处理”检查的异常,但他们是在谈论他们的帽子。在失败后继续使用空、不完整或不正确的数据来假装成功是不处理任何事情。这是最低形式的工程/可靠性弊端。

彻底失败是处理异常的最基本的正确策略。回滚事务、记录错误并向用户报告“失败”响应是合理的做法——最重要的是,防止将不正确的业务数据提交到数据库。

其他异常处理策略是在业务、子系统或请求级别的“重试”、“重新连接”或“跳过”。所有这些都是通用的可靠性策略,并且在运行时异常下工作得很好/更好。

最后,失败比使用不正确的数据运行要好得多。继续会导致次要错误,远离原始原因并且更难调试;或者最终会导致错误的数据被提交。人们因此而被解雇。

请参阅:
- http://literatejava.com/exceptions/checked-exceptions-javas-biggest-mistake/


我的观点是正确地失败作为一般策略。未经检查的异常有助于实现这一点,因为它们不会强制插入 catch 块。然后可以将捕获和错误记录留给一些最外层的处理程序,而不是在整个代码库中错误编码数千次(这实际上是隐藏错误的原因)。对于任意故障,未经检查的异常绝对是最正确的。意外事件——可预测的结果,例如资金不足——是唯一值得检查的例外情况。
我上面的回答已经解决了这个问题。首先,1)最外层的故障处理程序应该捕获所有内容。除此之外,仅针对特定的已识别站点,2) 可以捕获和处理特定的预期意外事件——在它们被抛出的直接站点。这意味着找不到文件、资金不足等,这些都可以从这些地方恢复——不会更高。封装原则意味着外层不能/不应该负责理解/从内部深处的故障中恢复。第三,3)其他所有东西都应该向外扔——如果可能的话,不要检查。
最外层处理程序捕获异常,记录它,并返回“失败”响应或显示错误对话框。很简单,一点也不难定义。关键是,由于封装原则,每个不能立即和本地恢复的异常都是不可恢复的故障。如果要知道的代码无法恢复它,那么整个请求就会完全正确地失败。这是正确执行此操作的正确方法。
不正确。最外层处理程序的工作是彻底失败并在“请求”边界上记录错误。中断请求正常失败,报告异常,线程能够继续服务下一个请求。这种最外层的处理程序是 Tomcat、AWT、Spring、EJB 容器和 Java“主”线程中的标准功能。
为什么在请求边界或最外层处理程序报告“真正的错误”很危险???我经常从事系统集成和可靠性工作,其中正确的可靠性工程实际上很重要,并使用“未经检查的异常”方法来做到这一点。我不太确定您实际上在辩论什么——似乎您可能想以未经检查的异常方式实际花费 3 个月,感受一下,然后也许我们可以进一步讨论。谢谢。
o
oxbow_lakes

我认为这是一个很好的问题,完全没有争论。我认为第 3 方库应该(通常)抛出 unchecked 异常。这意味着您可以隔离您对库的依赖项(即您不必重新抛出它们的异常或抛出 Exception - 通常是不好的做法)。 Spring 的 DAO layer 就是一个很好的例子。

另一方面,来自核心 Java API 的异常通常应该被检查是否可以曾经被处理。拿FileNotFoundException或(我最喜欢的)InterruptedException。这些条件几乎总是应该被专门处理(即您对 InterruptedException 的反应与您对 IllegalArgumentException 的反应不同)。检查您的异常这一事实迫使开发人员考虑条件是否可以处理。 (也就是说,我很少看到 InterruptedException 处理得当!)

还有一件事 - RuntimeException 并不总是“开发人员出错的地方”。当您尝试使用 valueOf 创建 enum 并且没有该名称的 enum 时,将引发非法参数异常。这不一定是开发人员的错误!


是的,这是开发商的错误。他们显然没有使用正确的名称,所以他们必须回去修复他们的代码。
@AxiomaticNexus 没有理智的开发人员使用 enum 成员名称,仅仅是因为他们使用 enum 对象。所以错误的名称只能来自外部,无论是导入文件还是其他。处理此类名称的一种可能方法是调用 MyEnum#valueOf 并捕获 IAE。另一种方法是使用预填充的 Map<String, MyEnum>,但这些是实现细节。
@maaartinus 在某些情况下,使用枚举成员名称而没有来自外部的字符串。例如,当您想动态循环所有成员以对每个成员执行某些操作时。此外,字符串是否来自外部无关紧要。开发人员拥有他们需要的所有信息,以了解将 x 字符串传递给“MyEnum#valueOf”是否会在传递之前导致错误。无论如何,将 x 字符串传递给“MyEnum#valueOf”会导致错误,这显然是开发人员的错误。
P
Powerlord

已检查异常的一个问题是,即使该接口的一个实现使用它,异常也经常附加到接口的方法上。

检查异常的另一个问题是它们容易被滥用。 java.sql.Connectionclose() 方法就是一个完美的例子。即使您已经明确声明您已完成连接,它也会抛出 SQLException。 close() 可能传达您关心的哪些信息?

通常,当我关闭()一个连接*时,它看起来像这样:

try {
    conn.close();
} catch (SQLException ex) {
    // Do nothing
}

另外,不要让我开始使用各种解析方法和 NumberFormatException ...... .NET 的 TryParse,它不会抛出异常,使用起来非常容易,不得不回到 Java 是很痛苦的(我们同时使用 Java 和C# 我工作的地方)。

*作为补充说明,PooledConnection 的 Connection.close() 甚至不会关闭连接,但您仍然必须捕获 SQLException,因为它是一个已检查的异常。


是的,任何驱动程序都可以......问题是“程序员为什么要关心?”反正他已经完成了对数据库的访问。文档甚至警告您,在调用 close() 之前,您应该始终 commit() 或 rollback() 当前事务。
许多人认为关闭文件不会引发异常... stackoverflow.com/questions/588546/… 您是否 100% 确定没有任何情况会发生这种情况?
我 100% 确定没有任何情况会重要,并且调用者不会放入 try/catch。
马丁,关闭连接的绝佳例子!我只能换个说法:如果我们只是明确表示我们已经完成了连接,为什么在我们关闭它时还要打扰正在发生的事情。还有更多这样的情况,程序员并不真正关心是否发生异常,而且他是绝对正确的。
@PiotrSobczyk:如果在启动事务后关闭连接但既不确认也不回滚,某些 SQL 驱动程序会发出尖叫声。恕我直言,尖叫比默默地忽略问题要好,至少在尖叫不会导致其他异常丢失的情况下。
A
Aleksandr Dubinsky

程序员需要知道方法可能抛出的所有异常,才能正确使用它。所以,仅仅用一些例外来击败他并不一定能帮助粗心的程序员避免错误。

繁重的成本超过了微薄的好处(尤其是在较大、不太灵活的代码库中,不断修改接口签名是不切实际的)。

静态分析可能很好,但真正可靠的静态分析通常不灵活地要求程序员进行严格的工作。有一个成本效益计算,并且需要将标准设置为高,以进行导致编译时错误的检查。如果 IDE 承担传达方法可能抛出哪些异常(包括哪些是不可避免的)的角色,将会更有帮助。尽管如果没有强制异常声明它可能不会那么可靠,但大多数异常仍然会在文档中声明,并且 IDE 警告的可靠性并不那么重要。


M
Michael-O

不需要 Checked Exception 的好证明是:

很多框架都为 Java 工作。就像 Spring 将 JDBC 异常包装到未经检查的异常,向日志中抛出消息一样 Java 之后出现的许多语言,甚至在 Java 平台上也是如此 - 他们不使用它们 检查的异常,这是对客户端如何使用代码的善意预测引发异常。但是编写此代码的开发人员永远不会知道代码客户端正在工作的系统和业务。例如,强制抛出已检查异常的接口方法。系统上有 100 个实现,50 个甚至 90 个实现都不会抛出这个异常,但是如果用户引用该接口,客户端仍然必须捕获这个异常。那些 50 或 90 个实现倾向于在它们自己内部处理这些异常,将异常放入日志(这对它们来说是很好的行为)。我们应该怎么做?我最好有一些背景逻辑来完成所有这些工作 - 将消息发送到日志。如果我,作为代码的客户,会觉得我需要处理异常 - 我会这样做。我可能会忘记它,对 - 但如果我使用 TDD,我的所有步骤都会被覆盖,我知道我想要什么。另一个例子,当我在 java 中使用 I/O 时,如果文件不存在,它会强制我检查所有异常?我该怎么办?如果不存在,系统将不会进行下一步。此方法的客户端不会从该文件中获得预期的内容 - 他可以处理运行时异常,否则我应该首先检查检查异常,将消息写入日志,然后从方法中抛出异常。不...不 - 我最好使用 RuntimeEception 自动执行它,它会自动亮起。手动处理它没有任何意义 - 我很高兴在日志中看到一条错误消息(AOP 可以帮助解决这个问题.. 修复 java 的东西)。如果最终我认为系统应该向最终用户显示弹出消息 - 我会显示它,这不是问题。

如果在使用核心库(如 I/O)时,java 可以让我选择使用什么,我会很高兴。 Like 提供了两个相同类的副本——一个用 RuntimeEception 包装。然后我们可以比较人们会使用什么。不过,就目前而言,许多人最好选择基于 java 或其他语言的一些框架。像 Scala、JRuby 什么的。许多人只是相信 SUN 是对的。


而不是有两个版本的类,应该有一种简洁的方法来指定代码块进行的任何方法调用都不会引发某些类型的异常,并且任何此类异常都应该通过某些指定的方式进行包装,并且重新抛出(默认情况下,创建一个具有适当内部异常的新 RuntimeException)。不幸的是,让外部方法 throws 成为内部方法的异常比让它包装内部方法的异常更简洁,因为后者的操作过程通常是正确的。
J
Jacob Toronto

我们已经看到了一些对 C# 首席架构师的引用。

以下是 Java 专家关于何时使用检查异常的另一种观点。他承认其他人提到的许多负面因素:Effective Exceptions


Java 中已检查异常的问题源于一个更深层次的问题,即太多信息被封装在异常的 TYPE 中,而不是实例的属性中。如果被“检查”是 throw/catch 站点的一个属性,并且如果可以以声明方式指定转义代码块的已检查异常是否应保留为已检查异常,或者被查看,则检查异常将很有用由任何封闭块作为未经检查的异常;同样,catch 块应该能够指定它们只需要检查的异常。
假设如果尝试访问不存在的键,则指定字典查找例程以引发某种特定类型的异常。客户端代码捕获此类异常可能是合理的。但是,如果查找例程使用的某些方法碰巧以查找例程不期望的方式抛出相同类型的异常,则客户端代码可能不应该捕获它。将检查性作为异常实例、抛出站点和捕获站点的属性将避免此类问题。客户端将捕获该类型的“已检查”异常,避开意外异常。
M
Mister Smith

尽管阅读了整页,我仍然找不到一个合理的反对检查异常的论据。大多数人都在谈论糟糕的 API 设计,无论是在某些 Java 类中还是在他们自己的类中。

此功能可能令人讨厌的唯一情况是原型设计。这可以通过向语言添加一些机制来解决(例如,一些@supresscheckedexceptions 注释)。但是对于常规编程,我认为检查异常是一件好事。


最佳实践“早扔,晚抓”与检查异常坚持立即处理它们是不相容的。它还阻止了 FP 函数式编程方法。请参阅:literatejava.com/exceptions/…
通过调用树的指数扩展实际上是坚持立即处理。如果仅将其应用于可预测且可能可恢复的意外事件,这可能是值得的,但“检查”行为被错误地扩大到广泛的不可预测和不可恢复的故障。 “文件打开”或“连接 JDBC”需要检查是合理的——大多数其他 IOException、SQLException、RemoteException 不是这样。这是 Java 库设计中的一个重大错误。请参阅我的答案和异常处理的基本入门。
“赶上”是基于可以隔离故障的级别——大多数时候,这些是业务/请求或出站连接/请求级别。简单而正确。
返回 null/false/-1 是不正确的做法,因为它错误地代表了您的客户的成功!这是一个严格的禁忌,因为它使执行能够继续执行不完整/无效/不正确的数据,以便稍后失败(坏)或提交到数据库(更糟)。如果业务逻辑的某些部分确实是可选的,而您没有说明,那么 try/catch 允许那些继续发出警告。无效值和在应用程序周围传播不良数据既不好也没有必要。
最佳实践异常处理实际上是基于如何最好地正确处理异常/错误(记录、报告,有时恢复)。这是一门科学,而不是一门艺术。获得 100% 最优和正确实际上是简单和容易的——只要我们不被(错误的库设计)推动“及早处理”。正如我们所看到的,这主要只是鼓励错误的练习。
a
adrian.tarau

我已经阅读了很多关于异常处理的内容,即使(大部分时间)我真的不能说我对检查异常的存在感到高兴或难过这是我的看法:低级代码中的检查异常(IO,网络、操作系统等)和高级 API/应用程序级别中的未经检查的异常。

即使它们之间没有那么容易划清界限,我发现在同一屋檐下集成多个 API / 库而不总是包装大量检查异常确实很烦人/很难,但另一方面,有时它被迫捕获一些异常并提供在当前上下文中更有意义的不同异常是有用/更好的。

我正在做的项目需要大量的库并将它们集成在同一个 API 下,API 完全基于未检查的异常。这个框架提供了一个高级 API,它在开始时充满了已检查的异常,只有几个未检查的异常异常(初始化异常,配置异常等),我必须说不是很友好。大多数时候,您必须捕获或重新抛出您不知道如何处理的异常,或者您甚至不关心(不要与您应该忽略异常混淆),尤其是在客户端click 可能会引发 10 个可能的(已检查)异常。

当前版本(第 3 个)仅使用未经检查的异常,并且它有一个全局异常处理程序,负责处理任何未捕获的内容。 API 提供了一种注册异常处理程序的方法,该处理程序将决定是否将异常视为错误(大多数情况下是这种情况),这意味着记录并通知某人,或者它可能意味着其他东西——比如这个异常,AbortException,这意味着中断当前的执行线程并且不记录任何错误,因为不希望这样做。当然,为了解决所有自定义线程必须使用 try {...} catch(all) 处理 run() 方法。

公共无效运行(){

try {
     ... do something ...
} catch (Throwable throwable) {
     ApplicationContext.getExceptionService().handleException("Handle this exception", throwable);
}

}

如果您使用 WorkerService 来安排作业(Runnable、Callable、Worker),这不是必需的,它会为您处理所有事情。

当然,这只是我的意见,可能不是正确的,但对我来说这似乎是一个好方法。我会在我发布项目后看到,如果我认为它对我有好处,对其他人也有好处...... :)