多年来,我一直无法得到以下问题的一个像样的答案:为什么一些开发人员如此反对受控异常?我有过无数次的对话,在博客上读过一些东西,读过Bruce Eckel说的话(我看到的第一个站出来反对他们的人)。

我目前正在编写一些新代码,并非常注意如何处理异常。我试图了解那些“我们不喜欢受控异常”的人的观点,但我仍然看不出来。

我的每一次谈话都以同样的问题结束。让我把它建立起来:

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

Error is for things that should never be caught (VM has a peanut allergy and someone dropped a jar of peanuts on it) RuntimeException is for things that the programmer did wrong (programmer walked off the end of an array) Exception (except RuntimeException) is for things that are out of the programmer's control (disk fills up while writing to the file system, file handle limit for the process has been reached and you cannot open any more files) Throwable is simply the parent of all of the exception types.

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

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

对于“我要做的就是退出”的参数,我说即使你退出了,你也需要显示一个合理的错误消息。如果你只是在处理错误上押注,那么当程序在没有明确说明原因的情况下退出时,你的用户不会太高兴。

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

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

人们从来不会回答的问题是:

如果抛出RuntimeException 子类代替异常 子类,你怎么知道 你应该去抓?

如果答案是catch Exception,那么您也在以与系统异常相同的方式处理程序员错误。在我看来这是不对的。

如果你捕获Throwable,那么你是在以同样的方式处理系统异常和虚拟机错误(等等)。在我看来这是不对的。

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

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

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

我不是在寻求什么时候使用这两种模型的建议,我想知道的是为什么人们从RuntimeException扩展,因为他们不喜欢从Exception扩展,和/或为什么他们捕获一个异常然后重新抛出一个RuntimeException,而不是将抛出添加到他们的方法中。我想了解不喜欢受控异常的动机。


当前回答

的确,受控异常一方面增加了程序的健壮性和正确性(你被迫对接口做出正确的声明——方法抛出的异常基本上是一种特殊的返回类型)。另一方面,你面临的问题是,由于异常“冒出来”,当你改变一个方法抛出的异常时,你经常需要改变大量的方法(所有的调用者,以及调用者的调用者,等等)。

Java中的受控异常不能解决后一个问题;c#和VB。我们把孩子和洗澡水一起倒掉。

OOPSLA 2005论文(或相关技术报告)中描述了一种折衷的好方法。

简而言之,它允许你说:方法g(x)像f(x)一样抛出,这意味着g抛出f抛出的所有异常。瞧,没有级联更改问题的受控异常。

虽然这是一篇学术论文,但我还是鼓励你阅读(部分)它,因为它很好地解释了受控异常的优点和缺点。

其他回答

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

So far, I have it found easier to work with the code base with Checked Exceptions. When I am using someone else's API, it is nice that I can see exactly what kind of error conditions I can expect when I call the code and handle them properly, either by logging, displaying or ignoring (Yes, there is valid cases for ignoring exceptions, such as a ClassLoader implementation). That gives the code I am writing an opportunity to recover. All runtime exceptions I propagate up until they are cached and handled with some generic error handling code. When I find a checked exception that I don't really want to handle at a specific level, or that I consider a programming logic error, then I wrap it into a RuntimeException and let it bubble up. Never, ever swallow an exception without a good reason (and good reasons for doing this are rather scarce)

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

当然,这完全取决于开发者的偏好和技能。编程和错误处理的两种方式可能同样有效(或无效),所以我不会说只有一种方法。

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

我知道这是一个老问题,但我花了一段时间与受控异常搏斗,我有一些东西要补充。请原谅我的长度!

我对受控异常的主要不满是它们破坏了多态性。让它们很好地与多态接口一起工作是不可能的。

以Java List界面为例。我们有常见的内存实现,比如ArrayList和LinkedList。我们还有骨架类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);
    }
}

By exposing the Widgets using the familiar List interface, you can retrieve items (list.get(123)) or iterate a list (for (Widget w : list) ...) without needing to know about WidgetList itself. One can pass this list to any standard methods that use generic lists, or wrap it in a Collections.synchronizedList. Code that uses it need neither know nor care whether the "Widgets" are made up on the spot, come from an array, or are read from a file, or a database, or from across the network, or from a future subspace relay. It will still work correctly because the List interface is correctly implemented.

但事实并非如此。上面的类不能编译,因为文件访问方法可能会抛出一个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。有点证明了我的观点!))

So checked exceptions simply don't work in cases like this. You can't throw them. Ditto for a clever Map backed by a database, or an implementation of java.util.Random connected to a quantum entropy source via a COM port. As soon as you try to do anything novel with the implementation of a polymorphic interface, the concept of checked exceptions fails. But checked exceptions are so insidious that they still won't leave you in peace, because you still have to catch and rethrow any from lower-level methods, cluttering the code and cluttering the stack trace.

我发现,无处不在的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中的受控异常的代码稍微简单一些。

高兴的是,扔t;语句在这里是合法的,尽管检查了t的类型,这要归功于Java 7中添加的关于重新抛出捕获的异常的规则。


When checked exceptions meet polymorphism, the opposite case is also a problem: when a method is spec'd as potentially throwing a checked exception, but an overridden implementation doesn't. For example, the abstract class OutputStream's write methods all specify throws IOException. ByteArrayOutputStream is a subclass that writes to an in-memory array instead of a true I/O source. Its overridden write methods cannot cause IOExceptions, so they have no throws clause, and you can call them without worrying about the catch-or-specify requirement.

但并非总是如此。假设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);
}

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

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

That rule causes some absurd problems. For example, one of the three write methods of OutputStream is not overridden by ByteArrayOutputStream. Specifically, write(byte[] data) is a convenience method that writes the full array by calling write(byte[] data, int offset, int length) with an offset of 0 and the length of the array. ByteArrayOutputStream overrides the three-argument method but inherits the one-argument convenience method as-is. The inherited method does exactly the right thing, but it includes an unwanted throws clause. That was perhaps an oversight in the design of ByteArrayOutputStream, but they can never fix it because it would break source compatibility with any code that does catch the exception -- the exception that has never, is never, and never will be thrown!

That rule is annoying during editing and debugging too. E.g., sometimes I'll comment out a method call temporarily, and if it could have thrown a checked exception, the compiler will now complain about the existence of the local try and catch blocks. So I have to comment those out too, and now when editing the code within, the IDE will indent to the wrong level because the { and } are commented out. Gah! It's a small complaint but it seems like the only thing checked exceptions ever do is cause trouble.


我快做完了。我对受控异常的最后一个不满是,在大多数调用站点上,没有什么有用的东西可以用它们来做。理想情况下,当出现问题时,我们应该有一个称职的特定于应用程序的处理程序,可以通知用户问题和/或适当地结束或重试操作。只有堆栈中较高的处理程序才能做到这一点,因为它是唯一知道总体目标的处理程序。

相反,我们得到了下面的习语,这是一种关闭编译器的猖獗方式:

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

在GUI或自动化程序中,打印的消息将不会被看到。更糟糕的是,它在异常之后继续执行其余代码。异常实际上不是一个错误吗?那就别印出来。否则,马上就会出现其他异常,此时原始异常对象将消失。这个习惯用法不比BASIC的On Error Resume Next或PHP的error_reporting(0);更好。

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

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

这就像e.p printstacktrace()一样懒惰;仍然在不确定的状态下继续编写代码。另外,特定日志系统或其他处理程序的选择是特定于应用程序的,因此这会影响代码重用。

但是等等!有一种简单而通用的方法可以找到特定于应用程序的处理程序。它位于调用堆栈的更高位置(或者它被设置为线程的未捕获异常处理程序)。因此,在大多数情况下,您所需要做的就是将异常抛出到堆栈的更高位置。例如,投掷;受控异常只会碍事。

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

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

微小的好处被繁重的成本所抵消(特别是在较大、不太灵活的代码库中,不断修改接口签名是不切实际的)。

Static analysis can be nice, but truly reliable static analysis often inflexibly demands strict work from the programmer. There is a cost-benefit calculation, and the bar needs to be set high for a check that leads to a compile time error. It would be more helpful if the IDE took on the role of communicating which exceptions a method may throw (including which are unavoidable). Although perhaps it would not be as reliable without forced exception declarations, most exceptions would still be declared in documentation, and the reliability of an IDE warning is not so crucial.

异常类

当谈到异常时,我总是会参考Eric Lippert的恼人异常博客文章。他将例外情况分为以下几类:

Fatal - These exceptions are not your fault: you cannot prevent then, and you cannot sensibly handle them. For example, OutOfMemoryError or ThreadAbortException. Boneheaded - These exceptions are your fault: you should have prevented them, and they represent bugs in your code. For example, ArrayIndexOutOfBoundsException, NullPointerException or any IllegalArgumentException. Vexing - These exceptions are not exceptional, not your fault, you cannot prevent them, but you'll have to deal with them. They are often the result of an unfortunate design decision, such as throwing NumberFormatException from Integer.parseInt instead of providing an Integer.tryParseInt method that returns a boolean false on parse failure. Exogenous - These exceptions are usually exceptional, not your fault, you cannot (reasonably) prevent them, but you must handle them. For example, FileNotFoundException.

API用户:

不能处理致命或愚蠢的异常。 应该处理令人烦恼的异常,但它们不应该出现在理想的API中。 必须处理外生异常。

已检查的异常

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

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

受控异常是必须处理的异常。处理异常就像吞下它一样简单。在那里!异常被处理。时期。如果开发者想这样处理,没问题。但他不能忽视这个例外,已经得到了警告。

API的问题

但是任何检查了令人烦恼和致命异常的API(例如JCL)都会给API用户带来不必要的压力。必须处理这样的异常,但是要么异常太常见,以至于一开始就不应该是异常,要么在处理它时什么都做不了。这导致Java开发人员讨厌受控异常。

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

结论

外生异常指的是那些不是您的错、无法预防、并且应该处理的异常。这些构成了所有可能引发的异常的一个小子集。api应该只检查外生异常,所有其他异常都不检查。这将使API更好,给API用户带来更少的压力,从而减少捕获所有、吞咽或重新抛出未经检查的异常的需要。

所以不要讨厌Java和它的受控异常。相反,讨厌那些过度使用受控异常的api。

这并不是反对受控异常的纯概念,但是Java用于受控异常的类层次结构是一个畸形秀。我们总是简单地称这些东西为“异常”——这是正确的,因为语言规范也这样称呼它们——但是异常在类型系统中是如何命名和表示的呢?

By the class Exception one imagines? Well no, because Exceptions are exceptions, and likewise exceptions are Exceptions, except for those exceptions that are not Exceptions, because other exceptions are actually Errors, which are the other kind of exception, a kind of extra-exceptional exception that should never happen except when it does, and which you should never catch except sometimes you have to. Except that's not all because you can also define other exceptions that are neither Exceptions nor Errors but merely Throwable exceptions.

哪些是“已检查”异常?可抛出异常是受控异常,除非它们也是错误,是未检查的异常,然后是异常,也是可抛出异常,是受控异常的主要类型,除了有一个例外,那就是if它们也是runtimeexception,因为那是另一种未检查的异常。

What are RuntimeExceptions for? Well just like the name implies, they're exceptions, like all Exceptions, and they happen at run-time, like all exceptions actually, except that RuntimeExceptions are exceptional compared to other run-time Exceptions because they aren't supposed to happen except when you make some silly error, although RuntimeExceptions are never Errors, so they're for things that are exceptionally erroneous but which aren't actually Errors. Except for RuntimeErrorException, which really is a RuntimeException for Errors. But aren't all exceptions supposed to represent erroneous circumstances anyway? Yes, all of them. Except for ThreadDeath, an exceptionally unexceptional exception, as the documentation explains that it's a "normal occurrence" and that that's why they made it a type of Error.

Anyway, since we're dividing all exceptions down the middle into Errors (which are for exceptional execution exceptions, so unchecked) and Exceptions (which are for less exceptional execution errors, so checked except when they're not), we now need two different kinds of each of several exceptions. So we need IllegalAccessError and IllegalAccessException, and InstantiationError and InstantiationException, and NoSuchFieldError and NoSuchFieldException, and NoSuchMethodError and NoSuchMethodException, and ZipError and ZipException.

只不过,即使检查了异常,也总有(相当简单的)方法可以欺骗编译器,在不检查的情况下抛出异常。如果你这样做,你可能会得到一个不确定的throwableexception,除非在其他情况下,它可能抛出一个意外的dexception,或一个未知的异常(与未知的错误无关,只针对“严重的异常”),或一个ExecutionException,或一个InvocationTargetException,或一个ExceptionInInitializerError。

哦,我们一定不能忘记Java 8时髦的新UncheckedIOException,这是一个RuntimeException异常,设计用来通过包装由I/O错误(不会引起IOError异常,尽管也存在)引起的已检查的IOException异常来抛出异常检查的概念,这些异常异常难以处理,因此您需要它们不被检查。

由于Java !