关于Java:反检查异常的案例

The case against checked exceptions

多年来,我一直无法对以下问题给出一个合理的答案:为什么有些开发人员如此反对检查异常?我有过无数次的交谈,在博客上读东西,读布鲁斯·埃克尔必须说的话(我看到的第一个人公开反对他们)。

我目前正在编写一些新的代码,并非常注意如何处理异常。我试着从"我们不喜欢被检查的例外"人群的角度来看问题,但我还是看不到。

每一次谈话都以同样的问题结束,没有人回答…我来设置一下:

通常(从Java是如何设计的)

  • 错误在于那些不应该被抓住的东西(VM对花生过敏,有人把一罐花生掉在上面)
  • RuntimeException是针对程序员做错的事情(程序员离开了数组的末尾)
  • 异常(runtimeexception除外)是指程序员无法控制的事情(写入文件系统时磁盘已满,已达到进程的文件句柄限制,无法再打开任何文件)
  • throwable只是所有异常类型的父级。

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

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

对于"我要做的就是退出"的论点,我说即使您退出,也需要显示一条合理的错误消息。如果您只是在处理错误上下赌注,那么当程序退出时,如果没有明确的指示原因,您的用户不会太高兴。

对于"它使重构变得困难"的人群来说,这意味着没有选择合适的抽象级别。与其声明方法抛出IOException,不如将IOException转换为更适合当前情况的异常。

我不存在用catch(exception)包装main的问题(或者在某些情况下,catch(throwable)以确保程序可以优雅地退出——但我总是捕获我需要的特定异常。这样做至少可以让我显示适当的错误消息。

人们从不回答的问题是:

If you throw RuntimeException
subclasses instead of Exception
subclasses then how do you know what
you are supposed to catch?

如果答案是catch exception,那么您也可以像处理系统异常一样处理程序员错误。我觉得这不对。

如果您捕获了可丢弃的,那么您将以相同的方式处理系统异常和VM错误(等等)。我觉得这不对。

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

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

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

编辑:我不想寻求关于何时使用这两种模型的建议,我要寻找的是为什么人们从RuntimeException扩展,因为他们不喜欢从异常扩展和/或为什么他们捕获一个异常,然后重新引发RuntimeException,而不是向他们的方法添加throw。我想了解不喜欢检查过的异常的动机。


我想我读了和你一样的布鲁斯·埃克尔的采访——这总是困扰着我。事实上,这场争论是被采访者(如果这真的是你所谈论的职位)anders hejlsberg提出的,她是.net和c背后的天才女士。好的。

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

Ok.

虽然我是希斯堡和他的作品的粉丝,但这场争论总是让我觉得是假的。基本上可以归结为:好的。

"Checked exceptions are bad because programmers just abuse them by always catching them and dismissing them which leads to problems being hidden and ignored that would otherwise be presented to the user".

Ok.

我的意思是,如果您使用运行时异常,懒惰的程序员将忽略它(而不是用空的catch块捕获它),并且用户将看到它。好的。

总结的论点是,"程序员不能正确地使用它们,并且不能正确地使用它们比没有它们更糟糕"。好的。

这一论点有一定的道理,事实上,我怀疑小鹅不会在Java中引入操作符重写的动机来自于一个类似的论点——他们混淆了程序员,因为他们经常被滥用。好的。

但最后,我发现这是一个虚假的Hejlsberg的论点,可能是一个事后的论点,用来解释这种缺乏,而不是一个经过深思熟虑的决定。好的。

我会争辩说,虽然过度使用检查异常是一件坏事,而且往往会导致用户处理的草率,但是正确使用这些异常可以让API程序员为API客户机程序员带来巨大的好处。好的。

现在,API程序员必须小心,不要到处抛出检查过的异常,否则它们只会使客户机程序员恼火。正如Hejlsberg警告的那样,非常懒惰的客户机程序员将求助于抓到(Exception) {},所有的好处都将丢失,地狱将随之而来。但在某些情况下,没有什么可以替代检查良好的异常。好的。

对于我来说,经典的例子是文件打开API。语言历史上的每一种编程语言(至少在文件系统上)都有一个API,可以让您在某个地方打开一个文件。而且每个使用这个API的客户机程序员都知道他们必须处理他们试图打开的文件不存在的情况。让我换个说法:每个使用此API的客户机程序员都应该知道他们必须处理这种情况。还有一个问题:API程序员是否可以通过单独评论来帮助他们了解应该如何处理它,或者他们是否确实可以坚持让客户机处理它。好的。

在C语言中,这个成语有点像好的。

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

其中fopen通过返回0表示失败,而c(愚蠢地)允许您将0视为布尔值,并且…基本上,你学会了这个习语,你就没事了。但如果你是个笨蛋,却没有学会这个成语怎么办?那么,当然,你从好的。

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

学习艰辛的方法。好的。

注意,我们这里只讨论强类型语言:有一个明确的概念,即一个API在强类型语言中是什么:它是一个功能(方法)的无组织文件,您可以用一个明确定义的协议来为每种语言使用。好的。

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

在某些(模糊的)语言中,返回类型也是协议的一部分。如果在某些语言中尝试调用等效的fopen(),而不将其赋给变量,则还会出现编译时错误(只能使用void函数)。好的。

我想说的是:在静态类型语言中,如果客户端代码出现明显错误,API程序员会通过阻止客户端代码编译来鼓励客户端正确地使用API。好的。

(在一种动态类型语言中,比如Ruby,您可以传递任何东西,比如一个float,作为文件名——它将编译。如果您甚至不想控制方法参数,为什么还要用选中的异常来麻烦用户呢?此处的参数仅适用于静态类型语言。)好的。

那么,检查异常呢?好的。

这里有一个Java API可以用来打开一个文件。好的。

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

看到了吗?下面是该API方法的签名:好的。

1
2
public FileInputStream(String name)
                throws FileNotFoundException

注意,FileNotFoundException是一个选中的异常。好的。

API程序员对您这样说:"可以使用此构造函数创建新的fileinputstream,但是好的。

a)必须将文件名作为弦b)必须接受文件可能不会在运行时找到"好的。

这就是我所关心的全部问题。好的。

关键是问题的状态基本上是"程序员无法控制的事情"。我的第一个想法是,他/她指的是API程序员无法控制的事物。但事实上,正确使用时检查的异常应该是客户机程序员和API程序员都无法控制的情况。我认为这是避免滥用检查异常的关键。好的。

我认为打开的文件很好地说明了这一点。API程序员知道你可能会给他们一个在调用API时并不存在的文件名,他们将无法返回你想要的,但必须抛出一个异常。他们还知道这种情况会经常发生,客户机程序员可能会期望在编写调用时文件名是正确的,但在运行时也可能是错误的,原因超出了他们的控制范围。好的。

因此,API明确表示:在你给我打电话的时候,有些情况下,这个文件不存在,你最好处理好它。好的。

这一点在反诉中会更清楚。假设我正在编写一个表API。我在某个地方有一个包含以下方法的API表模型:好的。

1
public RowData getRowData(int row)

现在作为一个API程序员,我知道有些情况下,有些客户机会传递行的负值或表外的行值。因此,我可能会抛出一个选中的异常并强制客户机处理它:好的。

1
public RowData getRowData(int row) throws CheckedInvalidRowNumberException

(我当然不会称之为"检查过的")。好的。

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

API程序员仍然可以预测客户机将对这些错误进行编码,并应使用运行时异常(如IllegalArgumentException)对其进行处理。好的。

getRowData中有一个选中的异常,很明显这是一个将导致Hejlsberg懒惰的程序员简单地添加空捕获的情况。当这种情况发生时,即使测试人员或客户机开发人员调试也不会发现非法的行值,相反,它们将导致难以确定其来源的连锁错误。阿里安娜火箭将在发射后爆炸。好的。

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

我想这不是一个确切的科学,我想这是基础,也许在一定程度上证明了赫杰斯伯格的论点。但是我不喜欢把孩子和洗澡水一起扔出去,所以请允许我在这里提取一些规则来区分好的检查异常和坏的检查异常:好的。

  • 超出客户控制或关闭与打开:好的。

    只有当错误情况超出API和客户机程序员的控制范围时,才应使用选中的异常。这与系统的打开或关闭程度有关。在一个受约束的UI中,客户机程序员可以控制所有按钮、键盘命令等,这些按钮、键盘命令等可以从表视图(一个封闭的系统)中添加和删除行,如果它试图从一个不存在的行中获取数据,那么这就是客户机编程错误。在一个基于文件的操作系统中,任何数量的用户/应用程序都可以添加和删除文件(一个开放系统),可以想象客户机请求的文件已经被删除,而他们不知道,因此他们应该处理它。好的。

  • 无所不在:好的。

    选中的异常不应用于客户端频繁进行的API调用。我经常指的是客户机代码中的许多地方——不经常是在时间上。因此,客户机代码不会经常尝试打开同一个文件,但是我的表视图使用不同的方法在整个地方获取RowData。尤其是,我要写很多类似的代码好的。

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

    每次都要尝试/捕获,这会很痛苦。好的。

  • 通知用户:好的。

    在可以想象向最终用户显示有用的错误消息的情况下,应该使用选中的异常。这就是"当它发生时你会怎么做?"我在上面提出的问题。还涉及第1项。由于您可以预测客户机API系统之外的某些内容可能会导致文件不在其中,因此您可以合理地告诉用户:好的。

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

    由于您的非法行数是由内部错误引起的,而且用户没有任何错误,所以您实际上无法提供有用的信息。如果你的应用程序不允许运行时异常进入控制台,它可能会给它们一些难看的信息,比如:好的。

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

    简而言之,如果您认为您的客户机程序员不能以一种帮助用户的方式解释您的异常,那么您可能不应该使用选中的异常。好的。

  • 所以这是我的规则。有点做作,肯定会有例外(如果你愿意,请帮助我完善它们)。但我的主要论点是,在一些情况下,比如FileNotFoundException,选中的异常与参数类型一样重要,也是API契约中有用的一部分。所以我们不应该仅仅因为它被滥用就放弃它。好的。

    抱歉,我不是故意这么长时间胡扯的。最后,我提出两个建议:好的。

    A:API程序员:谨慎地使用检查过的异常以保持它们的有用性。如果有疑问,请使用未经检查的异常。好的。

    B:客户机程序员:在开发早期养成创建一个打包的异常(google it)的习惯。JDK1.4及更高版本在RuntimeException中为此提供了一个构造函数,但您也可以轻松地创建自己的构造函数。建造师如下:好的。

    然后养成这样的习惯:每当你必须处理一个检查过的异常,而你觉得自己很懒惰(或者你认为API程序员在一开始就过分热衷于使用检查过的异常),不要仅仅吞下这个异常,包装它,然后重新处理它。好的。

    1
    2
    3
    4
    5
    6
    try {
      overzealousAPI(thisArgumentWontWork);
    }
    catch (OverzealousCheckedException exception) {
      throw new RuntimeException(exception);  
    }

    把它放在你的一个IDE的小代码模板中,当你觉得懒惰的时候使用它。这样,如果您真的需要处理检查过的异常,您将不得不在运行时看到问题后返回并处理它。因为,相信我(还有安德斯·海斯伯格),你永远不会在你的好的。

    1
    catch (Exception e) { /* TODO deal with this at some point (yeah right) */}

    好啊。


    关于检查异常的事情是,根据对概念的通常理解,它们并不是真正的异常。相反,它们是API可选返回值。

    异常的整体概念是,在调用链的某个地方抛出的错误可能冒泡,并由更高的某个地方的代码处理,而不必担心干预代码。另一方面,检查异常要求抛投者和接球者之间的每一级代码声明他们知道可以通过它们的所有异常形式。实际上,这与if-checked异常只是调用方必须检查的特殊返回值几乎没有什么不同。例如[伪代码]:

    1
    2
    3
    4
    5
    6
    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异常发生,如果我不做任何处理,应用程序将停止,我将得到一个方便的堆栈跟踪来查看。这是可能发生的最好的事情。

    可能有两种方法,从整个写到流过程中捕获所有的异常,中止进程并跳进错误报告代码;在Java中,不需要在每个调用级别添加"抛出IOExtExchange",即使它们本身不执行IO级别也不能这样做。这种方法不需要吗?了解异常处理;必须在其签名中添加异常:

  • 不必要地增加耦合;
  • 使接口签名很容易更改;
  • 使代码的可读性降低;
  • 非常烦人,程序员通常的反应是做一些可怕的事情来击败系统,比如"抛出异常"、"捕获(异常E)",或者将所有东西包装在RuntimeException中(这使得调试更加困难)。
  • 还有很多可笑的图书馆例外,比如:

    1
    2
    3
    4
    5
    try {
        httpconn.setRequestMethod("POST");
    }?catch (ProtocolException e) {
        throw new CanNeverHappenException("oh dear!");
    }

    当你不得不用这样可笑的代码混乱你的代码时,难怪被检查的异常会收到一堆讨厌的东西,即使这真的只是简单糟糕的API设计。

    另一个特别的不良影响是控制反转,其中组件A向通用组件B提供回调。组件A希望能够让异常从其回调中抛出到它调用组件B的位置,但它不能这样做,因为这会更改由B修复的回调接口。A只能通过wrappi来完成。在runtimeexception中生成真正的异常,这是一个需要编写的异常处理样板。

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


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

    1
    2
    3
    4
    5
    try {
      // do stuff
    } catch (AnnoyingcheckedException e) {
      throw new RuntimeException(e);
    }

    99%的时候我什么都做不到。最后,块进行任何必要的清理(或者至少应该进行清理)。

    我也不知道我见过多少次了:

    1
    2
    3
    4
    5
    try {
      // do stuff
    } catch (AnnoyingCheckedException e) {
      // do nothing
    }

    为什么?因为有人不得不处理它,而且很懒惰。这是错的吗?当然。发生了吗?当然。如果这是未检查的异常呢?这个应用程序可能已经死了(这比吞下一个例外要好)。

    然后我们有一些恼人的代码,将异常作为流控制的一种形式,就像java.text.format那样。Bzzzt。错了。用户在表单的数字字段中输入"abc"也不例外。

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


    好吧,这不是关于显示stacktrace或无声崩溃。它是关于能够在层之间传递错误。

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

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

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

    这就是为什么我非常喜欢Python中的异常处理。你只能抓住你想要和/或能处理的东西。其他的一切都会冒泡起来,就好像你自己把它重新穿上一样(不管你做了什么)。

    我发现,在我提到的整个项目中,异常处理分为三类:

  • 捕获并处理特定的异常。例如,这是为了实现重试逻辑。
  • 捕获并重新引发其他异常。这里所发生的一切通常是日志记录,它通常是一条陈腐的消息,比如"无法打开$filename"。这些都是你做不到的错误;只有更高层次的人知道足够的能力来处理它。
  • 捕获所有内容并显示错误消息。这通常位于调度器的根目录,它所做的一切就是确保它能够通过非异常机制(弹出对话框、封送RPC错误对象等)将错误传递给调用者。

  • 我知道这是一个古老的问题,但我花了一段时间与检查过的异常进行斗争,我还有一些补充。请原谅我这么久!好的。

    我的主要观点是他们破坏了多态性。不可能让它们很好地处理多态接口。好的。

    采用好的OL’Java EDCOX1×0接口。我们有常见的内存实现,如ArrayListLinkedList。我们还有骨骼类AbstractList,这使得设计新类型的列表很容易。对于只读列表,我们只需要实现两种方法:size()get(int index)。好的。

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

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    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本身无法处理这个异常(我将在后面解释)。好的。

    显然,唯一要做的就是捕获并重新将选中的异常作为某些未选中的异常来显示:好的。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    @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为这个情况添加了一个EDCOX1×17的类:用于捕获和重新抛出多态性方法边界中的EDCOX1、14个s。有点证明我的观点!)好的。

    所以检查异常在这种情况下根本不起作用。你不能扔它们。同样,对于由数据库支持的聪明的Map,或者通过COM端口连接到量子熵源的java.util.Random的实现。一旦您尝试对多态接口的实现做任何新颖的事情,检查异常的概念就失败了。但是检查过的异常是如此的阴险,以至于它们仍然不能让您安心,因为您仍然需要从较低级别的方法中捕获并重新传递任何异常,混乱代码和混乱堆栈跟踪。好的。

    我发现普遍存在的Runnable接口如果调用了抛出检查异常的东西,通常会返回到这个角。它不能按原样抛出异常,所以它所能做的就是通过捕获和重新执行RuntimeException来混乱代码。好的。

    实际上,如果你诉诸于黑客,你可以抛出未声明的检查异常。在运行时,JVM不关心检查的异常规则,因此我们只需要愚弄编译器。最简单的方法就是滥用仿制药。这是我的方法(类名是因为(在Java 8之前)在泛型方法的调用语法中需要的)显示的:好的。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    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"示例:好的。

    1
    2
    3
    4
    5
    6
    7
    8
    @Override
    public int size() {
        try {
            return (int)(file.length() / SIZE_OF_WIDGET);
        } catch (IOException e) {
            throw sneakyThrow(e);
        }
    }

    不幸的是,检查异常的最后一个侮辱是编译器拒绝允许您捕获检查异常,如果在它有缺陷的观点中,它不能被抛出。(未选中的异常没有此规则。)要捕获偷偷抛出的异常,必须执行以下操作:好的。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    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中的已检查异常的代码简单一些。好的。

    令人高兴的是,尽管在Java 7中添加了一个关于重新捕获异常的规则,但是EDCOX1 25的声明在这里是合法的,尽管EDCOX1 OR 26的类型被检查。好的。

    当选中的异常满足多态性时,相反的情况也是一个问题:当一个方法被指定为可能会抛出选中的异常,但被重写的实现却没有。例如,抽象类OutputStreamwrite方法都指定throws IOException方法。ByteArrayOutputStream是一个子类,它写入内存中的数组而不是真正的I/O源。它重写的write方法不能引起IOExceptions,因此它们没有throws子句,您可以调用它们而不必担心捕获或指定要求。好的。

    但并非总是如此。假设Widget有一种方法将其保存到流中:好的。

    1
    public void writeTo(OutputStream out) throws IOException;

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

    1
    2
    3
    4
    5
    6
    7
    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调用OutputStream所抛出的任何IOException,但您希望将变量的声明类型更改为ByteArrayOutputStream,编译器将责怪您试图捕获它所说的不能抛出的已检查异常。好的。

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

    这条规则在编辑和调试期间也很烦人。例如,有时我会临时注释一个方法调用,如果它可能引发了一个检查过的异常,编译器现在会抱怨本地trycatch块的存在。所以我也要把它们注释掉,现在在编辑代码时,IDE会缩进到错误的级别,因为{}被注释掉了。啊!这只是一个小小的抱怨,但似乎唯一被检查过的例外情况就是引起麻烦。好的。

    我快做完了。我最后对检查的异常感到失望的是,在大多数呼叫站点,您对它们没有任何帮助。理想情况下,当出现问题时,我们会有一个特定于应用程序的处理程序,它可以通知用户问题和/或根据需要结束或重试操作。只有栈中高层的处理程序才能做到这一点,因为它是唯一知道总体目标的处理程序。好的。

    相反,我们得到了下面的成语,它作为关闭编译器的一种方式非常猖獗:好的。

    1
    2
    3
    4
    5
    try {
        ...
    } catch (SomeStupidExceptionOmgWhoCares e) {
        e.printStackTrace();
    }

    在图形用户界面或自动程序中,不会看到打印的消息。更糟糕的是,它在异常之后继续处理其余的代码。异常实际上不是错误吗?那就不要打印了。否则,其他东西将在一瞬间爆炸,到那时原始异常对象将消失。这个习语不比basic的On Error Resume Next或php的error_reporting(0);好。好的。

    调用某种类型的logger类并不好:好的。

    1
    2
    3
    4
    5
    try {
        ...
    } catch (SomethingWeird e) {
        logger.log(e);
    }

    这和e.printStackTrace();一样懒惰,仍然在不确定的状态下处理代码。另外,特定日志记录系统或其他处理程序的选择是特定于应用程序的,因此这会影响代码的重用。好的。

    但是等等!找到特定于应用程序的处理程序有一种简单而通用的方法。它位于调用堆栈的上方(或者设置为线程的未捕获异常处理程序)。所以在大多数情况下,您需要做的就是将异常抛到堆栈的更高位置。例如,throw e;。选中的异常会妨碍您的工作。好的。

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


    信噪比

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

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

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    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:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    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的列表接口,检查异常作为合同失效的设计工具。考虑一个由数据库、文件系统或任何其他抛出检查异常的实现支持的列表。唯一可能的实现是捕获选中的异常并将其作为未选中的异常重新引发:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    @Override
    public void clear()  
    {  
       try  
       {  
           backingImplementation.clear();  
       }  
       catch (CheckedBackingImplException ex)  
       {  
           throw new IllegalStateException("Error clearing underlying list.", ex);  
       }  
    }

    现在你必须问,所有这些代码的意义是什么?选中的异常只会增加噪声,异常已被捕获但未被处理,合同设计(根据选中的异常)已被破坏。

    结论

    • 捕获异常与处理异常是不同的。
    • 选中的异常会给代码添加噪声。
    • 异常处理在没有它们的情况下在C中工作得很好。

    我以前在博客上写过这个。


    Artima发表了一篇对.NET的一位架构师AndersHejlsberg的采访,其中尖锐地涵盖了反对检查异常的论点。短品酒师:

    The throws clause, at least the way it's implemented in Java, doesn't necessarily force you to handle the exceptions, but if you don't handle them, it forces you to acknowledge precisely which exceptions might pass through. It requires you to either catch declared exceptions or put them in your own throws clause. To work around this requirement, people do ridiculous things. For example, they decorate every method with,"throws Exception." That just completely defeats the feature, and you just made the programmer write more gobbledy gunk. That doesn't help anybody.


    有效的Java异常可以很好地解释何时使用未选中的以及何时使用已检查的异常。以下是该文章的一些引言,以突出重点:

    Contingency:
    An expected condition demanding an alternative response from a method that can be expressed in terms of the method's intended purpose. The caller of the method expects these kinds of conditions and has a strategy for coping with them.

    Fault:
    An unplanned condition that prevents a method from achieving its intended purpose that cannot be described without reference to the method's internal implementation.

    (因此不允许使用表,因此您可能希望从原始页中读取以下内容…)

    Contingency

    • Is considered to be: A part of the design
    • Is expected to happen: Regularly but rarely
    • Who cares about it: The upstream code that invokes the method
    • Examples: Alternative return modes
    • Best Mapping: A checked exception

    Fault

    • Is considered to be: A nasty surprise
    • Is expected to happen: Never
    • Who cares about it: The people who need to fix the problem
    • Examples: Programming bugs, hardware malfunctions, configuration mistakes,
      missing files, unavailable servers
    • Best Mapping: An unchecked exception


    最初我同意你的看法,因为我一直支持检查异常,并开始思考为什么我不喜欢在.NET中不检查异常。但后来我意识到我不会像检查过的异常那样犯错。

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

    原因是,如果它这样做了,我必须修复它,我必须马上修复它。我想马上知道有问题。

    您实际处理异常的次数是多少?我说的不是捕获异常——我说的是处理异常?写以下内容太容易了:

    1
    2
    3
    4
    5
    try {
      thirdPartyMethod();
    } catch(TPException e) {
      // this should never happen
    }

    我知道你可以说这是不好的做法,"答案"是做一些例外的事情(让我猜猜,记录下来?)但是在现实世界中,大多数程序员都不这样做。

    所以,是的,如果我不需要这样做的话,我不想捕获异常,我希望我的程序在我出错的时候爆炸得非常厉害。默默无闻的失败是最糟糕的结果。


    简而言之:

    例外是一个API设计问题。--不多不少。

    选中异常的参数:

    为了理解为什么选中的异常可能不是好事,让我们把问题转过来问:什么时候或者为什么选中的异常有吸引力,也就是说,为什么您希望编译器强制声明异常?

    答案是显而易见的:有时您需要捕获一个异常,并且只有当被调用的代码为您感兴趣的错误提供了一个特定的异常类时,这才是可能的。

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

    但实际上,常常一个包com.acme只抛出一个AcmeException,而不是特定的子类。然后,呼叫者需要处理、声明或重新发送AcmeExceptions信号,但仍然无法确定是发生了AcmeFileNotFoundError还是AcmePermissionDeniedError信号。

    因此,如果您只对AcmeFileNotFoundError感兴趣,那么解决方案是向acme程序员提交一个特性请求,并告诉他们实现、声明和记录AcmeException的子类。

    那为什么还要麻烦呢?

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

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

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

    如果你遵循这一推理,很明显,在Java等语言中如此常见的异常声明、捕获和重新抛出的"麻烦"通常没有什么价值。

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


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

    到目前为止,我发现使用带有检查异常的代码库更容易。当我使用其他人的API时,当我调用代码并通过日志记录、显示或忽略(是的,有忽略异常的有效案例,例如类加载器实现)正确地处理代码时,我可以准确地看到我所期望的错误条件。这给了我编写的代码一个恢复的机会。我传播的所有运行时异常直到它们被缓存并用一些通用错误处理代码处理。当我发现一个我不想在特定级别上处理的已检查异常,或者我认为是一个编程逻辑错误时,我将它包装成一个runtimeexception并让它冒泡。永远不要在没有充分理由的情况下接受例外情况(这样做的充分理由是相当少的)

    当我使用没有检查异常的代码库时,它会让我有点难以提前知道调用函数时会发生什么,这会严重破坏一些东西。

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

    总之,我发现使用检查异常更容易,特别是在有很多开发人员的大型项目中。


    例外类别

    在谈到例外的时候,我总是参考埃里克·利珀特的恼人的例外博客文章。他将例外情况分为以下几类:

    • 致命的-这些例外不是你的错:你不能阻止,你不能明智地处理它们。例如,OutOfMemoryErrorThreadAbortException
    • 这些异常是你的错:你应该阻止它们,它们在你的代码中代表错误。例如,ArrayIndexOutOfBoundsExceptionNullPointerException或任何IllegalArgumentException
    • 烦人-这些例外并不例外,不是你的错,你不能阻止它们,但你必须处理它们。它们通常是错误设计决策的结果,例如从Integer.parseInt中抛出NumberFormatException,而不是提供Integer.tryParseInt方法,该方法在解析失败时返回布尔值false。
    • 外生的-这些例外通常是例外的,不是你的错,你不能(合理地)阻止它们,但你必须处理它们。例如,FileNotFoundException

    API用户:

    • 不能处理致命或无效的异常。
    • 应该处理恼人的异常,但它们不应该出现在理想的API中。
    • 必须处理外部异常。

    已检查异常

    API用户必须处理特定异常的事实是调用方和被调用方之间的方法契约的一部分。约定指定了以下内容:被调用者期望的参数的数量和类型、调用者期望的返回值的类型以及调用者期望处理的异常。

    因为vexing异常不应该存在于API中,所以只有这些外部异常必须被检查为方法契约的一部分。相对较少的异常是外生的,所以任何API都应该有相对较少的检查异常。

    选中的异常是必须处理的异常。处理异常可能和吞咽异常一样简单。那里!处理异常。时期。如果开发人员想用这种方式处理,那就好。但他不能忽视这一例外,并受到警告。

    API问题

    但是任何检查过烦人和致命异常(如JCL)的API都会给API用户带来不必要的压力。必须处理此类异常,但要么异常非常常见,以至于在第一时间不应该是异常,要么在处理异常时什么也做不到。这导致Java开发人员讨厌被检查的异常。

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

    结论

    外生的例外是那些不是你的错,不可能被阻止,应该被处理的。这些构成了可以抛出的所有异常的一小部分。API应该只检查外部异常,而不检查所有其他异常。这将使API变得更好,减少对API用户的压力,从而减少捕获所有、吞咽或重新引发未检查异常的需要。

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


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

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

    Oopsla2005论文(或相关技术报告)中描述了一种走中间道路的好方法。

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

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


    好啊。。。检查异常并不理想,有一些警告,但它们确实有一定的用途。在创建API时,有一些特定的失败案例属于此API的契约。如果在一个强静态类型的语言(如Java)的上下文中,如果不使用已检查的异常,则必须依靠自组织文档和约定来传达错误的可能性。这样做会消除编译器在处理错误时带来的所有好处,而您将完全听从程序员的意愿。

    因此,删除检查过的异常(如在C中完成的异常),那么如何在编程和结构上传递错误的可能性呢?如何通知客户机代码可能会发生此类错误,并且必须加以处理?

    我听说在处理选中的异常时有各种各样的恐怖,它们被误用了这是肯定的,但未选中的异常也是如此。我说,等几年,当API被堆得很深的时候,你会乞求返回某种结构化的方法来传递失败。

    举个例子,当异常被抛出到API层底部的某个地方并冒泡了,因为没有人知道这个错误甚至有可能发生,尽管这是一种错误类型,当调用代码抛出它时是非常合理的(例如,fileNotFoundException,而不是vogonstrashingarTheException…在这种情况下,不管我们是否处理它都无关紧要,因为没有什么可以处理的了)。

    许多人认为,无法加载文件几乎总是这个过程的世界末日,它必须是一个可怕而痛苦的死亡。所以是的…当然。。。好啊。。您为某个东西构建了一个API,它在某个时刻加载文件…我作为上述API的用户只能响应…"你到底是谁来决定我的程序什么时候会崩溃!"当然,考虑到异常被吞噬而不留下任何痕迹的选择,或是比玛丽安娜沟更深的叠加痕迹的eletroflabingchunkfluxmanifoldchuggingexception,我会毫不犹豫地选择后者,但这是否意味着这是处理异常的理想方式?我们能不能不在中间的某个地方,每次异常遍历到一个新的抽象级别时,它都会被重铸和包装,这样它实际上就意味着什么呢?

    最后,我看到的大多数论点是"我不想处理异常,很多人不想处理异常"。检查异常迫使我去处理它们,因此我讨厌检查异常,"完全消除这种机制,把它放任到地狱的裂口,这是愚蠢的,缺乏灵活性和远见。

    如果我们消除检查的异常,我们也可以消除函数的返回类型,并始终返回"anytype"变量…那会让生活变得简单多了,不是吗?


    问题

    对于异常处理机制,我看到的最糟糕的问题是它在很大程度上引入了代码复制!让我们诚实一点:在95%的时间里,开发人员真正需要做的就是以某种方式将其传达给用户(在某些情况下,也包括传达给开发团队,例如通过发送带有堆栈跟踪的电子邮件)。因此,通常在处理异常的每个地方使用相同的代码行/代码块。

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

    1
    2
    3
    4
    5
    try{
       methodDeclaringCheckedException();
    }catch(CheckedException e){
       logger.error(e);
    }

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

    稍等片刻。。。我们真的要编辑代码中的数百个位置吗?!你明白我的意思:—)。

    解决方案

    我们对解决这个问题所做的是引入异常处理程序(我将进一步称为eh)的概念,以集中化异常处理。对于每个需要处理异常的类,依赖项注入框架都会注入异常处理程序的实例。因此,异常处理的典型模式如下:

    1
    2
    3
    4
    5
    try{
        methodDeclaringCheckedException();
    }catch(CheckedException e){
        exceptionHandler.handleError(e);
    }

    现在,为了定制我们的异常处理,我们只需要在一个地方更改代码(eh代码)。

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

    这样,我们就可以在应用程序的开发和发布版本中区分异常处理行为(例如,开发—记录错误并停止应用程序,生成—以更详细的信息记录错误并让应用程序继续执行),而无需付出任何努力。

    最后一件事

    最后但并非最不重要的是,似乎可以通过将异常"向上"传递到某个顶级异常处理类来实现相同的集中化。但这导致了代码和方法签名的混乱,并引入了这个线程中其他人提到的维护问题。


    安德斯在《软件工程无线电》第97集中谈到了检查过的异常的陷阱,以及为什么把它们排除在C之外的原因。


    试图解决未回答的问题:

    If you throw RuntimeException subclasses instead of Exception subclasses then how do you know what you are supposed to catch?

    这个问题包含了似是而非的推理。仅仅因为API告诉你它抛出了什么并不意味着你在所有情况下都用同样的方式处理它。换一种说法,您需要捕获的异常因使用抛出异常的组件的上下文而异。

    例如:

    如果我正在为数据库编写连接测试程序,或者为检查用户输入的xpath的有效性而编写一些东西,那么我可能希望捕获并报告操作引发的所有已检查和未检查的异常。

    但是,如果我正在编写一个处理引擎,我可能会像处理NPE一样处理xpathexception(选中):我会让它运行到工作线程的顶部,跳过该批的其余部分,记录问题(或将其发送到支持部门进行诊断),并为用户留下反馈以联系支持人员。


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

    事实上,如果你强制执行检查过的异常,那么你就不能强迫每个程序员"做正确的事情",而其他人现在都是附带的损害,他们只是因为你制定的规则而恨你。

    修复那里的坏程序!不要试图修复语言以不允许它们!对于大多数人来说,"做一些关于异常的事情"实际上就是告诉用户它。我也可以告诉用户一个未选中的异常,所以请将您选中的异常类从我的API中删除。


    我在c2.com上写的东西与它原来的形式基本上没有变化:a href="http://c2.com/cgi/wiki?"检查异常与可见模式不兼容">检查异常与可见模式不兼容/aa

    综上所述:

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

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

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

    至于根本问题:

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


    这篇文章是我读过的Java中异常处理的最好的一篇文章。

    它支持未经检查的异常,但这种选择的解释非常仔细,并且基于强有力的论据。

    我不想在这里引用太多的文章内容(最好把它作为一个整体来阅读),但它涵盖了这个主题中未经检查的异常倡导者的大多数论点。尤其是这个论点(似乎很流行)包括:

    Take the case when the exception was thrown somewhere at the bottom of the API layers and just bubbled up because nobody knew it was even possible for this error to occur, this even though it was a type of error that was very plausible when the calling code threw it (FileNotFoundException for example as opposed to VogonsTrashingEarthExcept... in which case it would not matter if we handle it or not since there is nothing left to handle it with).

    作者"回应":

    It is absolutely incorrect to assume that all runtime exceptions
    should not be caught and allowed to propagate to the very"top" of the
    application. (...) For every exceptional condition that is required to
    be handled distinctly - by the system/business requirements -
    programmers must decide where to catch it and what to do once the
    condition is caught. This must be done strictly according to the
    actual needs of the application, not based on a compiler alert. All
    other errors must be allowed to freely propagate to the topmost
    handler where they would be logged and a graceful (perhaps,
    termination) action will be taken.

    主要思想或文章是:

    When it comes to error handling in software, the only safe and correct assumption that may ever be made is that a failure may occur in absolutely every subroutine or module that exists!

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

    如此可悲,似乎没有多少人发现这篇伟大的文章。我全心全意推荐那些犹豫不决的人,哪种方法更适合花些时间来阅读。


    检查过的异常,在其最初的形式中,是一种处理意外事件而不是失败的尝试。值得称赞的目标是突出特定的可预测点(无法连接、找不到文件等),并确保开发人员处理了这些问题。

    最初的概念中从未包括的是强制宣布大量的系统性和不可恢复的故障。这些失败永远不会被声明为检查异常。

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

    一个关键点是,运行时和检查的异常在功能上是等效的。检查的异常无法处理或恢复,运行时异常无法处理或恢复。

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

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

    特别有问题的是JDBC(SQLException)和RMI for EJB(RemoteException)的领域。这些强制普遍存在的系统可靠性问题,实际上不是可修复的,而不是根据最初的"检查异常"概念来确定可修复的突发事件。

    Java设计中的另一个严重缺陷是异常处理应该正确地放置在尽可能高的"业务"或"请求"级别。这里的原则是"早抛,晚抓"。选中的异常几乎没有做什么,但会妨碍这一点。

    我们在Java中有一个明显的问题,即需要数千个无需尝试的catch块,其中相当大的比例(40% +)被错误编码。几乎所有这些都没有实现任何真正的处理或可靠性,但是会增加主要的编码开销。

    最后,"检查的异常"与fp函数编程非常不兼容。

    他们对"立即处理"的坚持与"延迟捕获"异常处理最佳实践以及抽象循环/或控制流的任何FP结构都不一致。

    许多人谈论"处理"检查过的异常,但他们是在自言自语。在出现错误(数据为空、不完整或不正确)后继续,以假装成功,这不处理任何事情。这是最低形式的工程/可靠性弊端。

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

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

    最后,与使用不正确的数据运行相比,失败要好得多。如果继续,将导致次要错误,与原始原因相距较远,更难调试,或者最终导致提交错误数据。人们为此被炒鱿鱼。

    见:-http://literatejava.com/exceptions/checked-exceptions-javas-most-error/


    这不是一个反对纯检查异常概念的论点,但是Java类使用的类层次结构是一个怪异的展示。我们总是简单地称之为"异常"——这是正确的,因为语言规范也这样称呼它们——但是在类型系统中如何命名和表示异常?

    按类Exception一个想象?不,因为Exception是例外,同样的例外是Exceptions,除了那些不是Exceptions的例外,因为其他例外实际上是Errors,这是另一种例外,一种不应该发生的额外例外,除非它发生,你应该除非有时你不得不这样做。但这并不是全部,因为您还可以定义其他既不是Exception也不是Error的异常,而只是Throwable的异常。

    哪些是"选中"的例外情况?Throwable是检查异常,除非它们也是Errors,这是未检查异常,然后还有Exceptions,这也是Throwables,是检查异常的主要类型,但也有一个例外,那就是如果它们也是RuntimeExceptions,因为这是另一种类型的unche禁止异常。

    RuntimeException是干什么用的?正如这个名字所暗示的,它们是例外,就像所有的Exceptions一样,它们发生在运行时,就像所有的例外一样,实际上,除了RuntimeExceptions与其他运行时Exceptions相比是例外,因为它们不应该发生,除非你犯了一些愚蠢的错误,尽管RuntimeExceptions从来不是Errors。S,所以它们是为了那些异常错误但实际上不是ErrorS的东西。除了RuntimeErrorException外,它实际上是RuntimeException对于ErrorS的东西。但是,所有的异常都不应该代表错误的情况吗?是的,所有的。除ThreadDeath外,这是一个例外的非例外情况,因为文件解释说这是一个"正常现象",这就是为什么他们把它变成Error的类型。

    不管怎样,由于我们将所有异常划分为Errors(对于异常执行异常,未选中)和Exceptions(对于较少的异常执行错误,除非没有异常执行错误,否则已选中),我们现在需要两种不同类型的异常。所以我们需要IllegalAccessErrorIllegalAccessExceptionInstantiationErrorInstantiationExceptionNoSuchFieldErrorNoSuchFieldExceptionNoSuchMethodErrorNoSuchMethodExceptionZipErrorZipException

    但是,即使在检查异常时,也总是有(相当容易)的方法欺骗编译器并在不检查异常的情况下抛出它。如果你这样做,你可以得到一个UndeclaredThrowableException,除非在其他情况下,它可以作为一个UnexpectedException,或一个UnknownException(与UnknownError无关,只是为了"严重的例外"),或一个ExecutionException,或一个InvocationTargetException或一个ExceptionInInitializerError

    哦,而且我们不能忘记Java 8的新的EDCOX1×44,这是一个EDCOX1×12的例外,旨在通过将由I/O错误引起的检查EDCOX1 46个异常(它不会引起EDCOX1,47个例外),将异常检查概念扔出窗口,尽管这也存在异常,这是非常难掌握的。所以你不需要检查它们。

    谢谢JAVA!


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

    检查异常的另一个问题是它们往往被误用。这一点的完美例子是在java.sql.Connectionclose()方法中。它可以抛出一个SQLException,即使您已经明确地声明您已经完成了连接。什么信息可以关闭()可能表示您关心的?

    通常,当我关闭()一个connection*时,它看起来是这样的:

    1
    2
    3
    4
    5
    try {
        conn.close();
    } catch (SQLException ex) {
        // Do nothing
    }

    另外,不要让我开始了解各种分析方法和NumberFormatException…NET的TyPARSE,它不会抛出异常,使用起来很容易,要返回Java是很痛苦的(我们使用Java和C i工作的地方)。

    *作为附加注释,pooledConnection的connection.close()甚至不会关闭连接,但您仍然需要捕获sqlException,因为它是一个选中的异常。


    程序员需要知道一个方法可能抛出的所有异常,以便正确地使用它。因此,仅仅用一些异常来殴打他并不一定能帮助粗心的程序员避免错误。

    这种微小的好处被繁重的成本所抵消(尤其是在更大、更不灵活的代码库中,不断修改接口签名是不现实的)。

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


    下面是一个反对检查异常的论点(来自joelonsoftware.com):

    The reasoning is that I consider exceptions to be no better than
    "goto's", considered harmful since the 1960s, in that they create an
    abrupt jump from one point of code to another. In fact they are
    significantly worse than goto's:

    • They are invisible in the source code. Looking at a block of code,
      including functions which may or may not throw exceptions, there is no
      way to see which exceptions might be thrown and from where. This means
      that even careful code inspection doesn't reveal potential bugs.
    • They create too many possible exit points for a function. To write correct
      code, you really have to think about every possible code path through
      your function. Every time you call a function that can raise an
      exception and don't catch it on the spot, you create opportunities for
      surprise bugs caused by functions that terminated abruptly, leaving
      data in an inconsistent state, or other code paths that you didn't
      think about.


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

    假设您定义了一个MyAppException extends Exception。它是由应用程序引发的所有异常继承的顶级异常。每个方法都声明throws MyAppException,这有点多余,但可以管理。异常处理程序记录异常并以某种方式通知用户。

    在您想要实现一些不是您的接口之前,一切看起来都正常。显然,它不声明抛出MyApException的意图,因此编译器不允许您从那里抛出异常。

    但是,如果您的异常扩展了RuntimeException,那么接口就不会有问题。如果愿意的话,您可以在javadoc中自愿地提到这个例外。但除此之外,它只是无声地冒泡通过任何东西,被捕获在您的异常处理层。


    良好的证据表明,不需要检查异常:

  • 很多为Java工作的框架。像Spring将JDBC异常包装为未检查的异常,将消息抛出日志
  • Java之后的许多语言,甚至在Java平台上都是顶级的——它们不使用它们。
  • 检查异常,这是关于客户端如何使用引发异常的代码的友好预测。但是编写这段代码的开发人员永远不会知道代码客户机所使用的系统和业务。作为一个例子,强制抛出检查异常的交互方法。系统上有100个实现,50个甚至90个实现都不会引发此异常,但是如果用户引用该接口,客户机仍然必须捕获此异常。这50或90个实现倾向于在自身内部处理这些异常,将异常放到日志中(这对它们来说是很好的行为)。我们该怎么办?我最好有一些后台逻辑来完成所有的工作——向日志发送消息。如果我作为代码的客户,觉得我需要处理这个异常,我会做的。我可能会忘记它,对吧-但如果我使用TDD,我的所有步骤都会被覆盖,我知道我想要什么。
  • 另一个例子,当我在Java中使用I/O时,它强迫我检查所有异常,如果文件不存在?我该怎么办?如果它不存在,系统将不会进入下一步。此方法的客户端无法从该文件中获取预期的内容-他可以处理运行时异常,否则我应该首先检查检查异常,将消息记录到日志中,然后从该方法中抛出异常。不…不-我最好用runtimeeception自动完成,这会自动完成。手动处理没有任何意义-我很高兴在日志中看到错误消息(AOP可以帮助解决这个问题)。修复Java的东西。如果,最终,我认为系统应该向最终用户显示弹出消息-我将显示它,而不是一个问题。
  • 我很高兴,如果Java能为我提供一个选择,当使用核心LIBS时,像I/O一样,它提供了两个相同类的副本,一个是RunTimeEclipse包装的。然后我们可以比较人们会使用什么。但是现在,许多人最好在Java或其他语言上寻找一些框架。就像斯卡拉,JRuby什么的。很多人只是相信太阳是对的。


    我认为这是一个很好的问题,完全没有争议。我认为第三方库应该(一般)抛出未经检查的异常。这意味着您可以隔离您对库的依赖(即,您不必重新抛出它们的异常或抛出Exception—通常是错误的做法)。春天的刀层就是一个很好的例子。

    另一方面,核心JavaAPI的异常通常应检查是否可以处理。以FileNotFoundException或(我最喜欢的)InterruptedException为例。这些条件几乎总是要特别处理(即,你对InterruptedException的反应与你对IllegalArgumentException的反应不同)。检查异常的事实迫使开发人员考虑一个条件是否可以处理。(也就是说,我很少看到InterruptedException处理得当!)

    还有一件事——RuntimeException并不总是"开发人员出了问题"。当你试图用valueOf创建一个enum并且没有该名称的enum时,就会抛出一个非法的参数异常。这不一定是开发人员的错误!


    我们已经看到一些关于C首席建筑师的参考资料。

    这里有一个Java人关于使用检查异常的替代观点。他承认其他人提到的许多否定:有效例外


    尽管读了整页,我还是找不到一个合理的理由来反对检查过的异常。大多数人讨论的是API设计不佳,无论是在Java类还是在自己的类中。

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


    我读过很多关于异常处理的文章,即使(大多数时候)我不能真的说我对检查过的异常的存在感到高兴或难过,这是我的看法:在低级代码(IO、网络、OS等)中检查过异常,在高级API/应用程序级别中检查过异常。

    即使在它们之间划一条线并不容易,但我发现在同一屋檐下集成多个API/库而不总是包装许多检查过的异常确实很烦人/困难,但另一方面,有时强制捕获一些异常并提供不同的异常会更有用/更好在当前上下文中有意义。

    我正在处理的项目需要很多库,并将它们集成在同一个API(完全基于未检查异常的API)下。这个框架提供了一个高层API,它在开始时充满了检查异常,并且只有几个未检查异常(初始化异常、配置异常等),我必须萨伊不是很友好。大多数情况下,您必须捕获或重新抛出您不知道如何处理的异常,或者您甚至不在乎(不要与您混淆,应忽略异常),特别是在客户端,单击一次可以抛出10个可能的(选中的)异常。

    当前版本(第三个版本)只使用未选中的异常,它有一个全局异常处理程序,负责处理任何未捕获的异常。API提供了一种注册异常处理程序的方法,该方法将决定是否将异常视为错误(大多数情况下是这样),这意味着日志和通知某人,或者它可能意味着其他事情,如此异常、异常异常,这意味着中断当前执行线程,不记录任何错误,因为不需要记录任何错误。T to。当然,为了计算出所有自定义线程必须使用try…catch(all)处理run()方法。

    public void run()。{

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

    }

    如果您使用WorkerService来计划作业(可运行、可调用、Worker),则不需要这样做,因为它可以为您处理所有事情。

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


    在我看来,检查异常是一个非常好的概念。不幸的是,大多数一起工作的程序员,我们有另一个观点,这样项目就有很多错误的使用异常处理。我已经看到大多数程序员创建了一个(只有一个)异常类,一个RuntimeException的子类。它包含一条消息,有时是一个多语言键。我没有机会反驳这一点。我的印象是,当我向他们解释反模式是什么,方法的契约是什么时,我会和一堵墙交谈……我有点失望。

    但是,今天已经很明显了,对所有东西都有一个通用的运行时异常的概念是反模式的。他们使用它来检查用户输入。抛出异常以便用户对话可以从中生成错误消息。但并非每个方法调用方都是对话!通过引发运行时异常,该方法的约定已更改但未声明,因为它不是已检查的异常。

    希望他们今天学到了一些东西,并且会在另一个地方进行检查(这是有用和必要的)。只使用检查的异常不能解决问题,但是检查的异常会向程序员发出信号,表明他实现了错误的东西。