The case against checked exceptions

2018-12-31 23:23发布

For a number of years now I have been unable to get a decent answer to the following question: why are some developers so against checked exceptions? I have had numerous conversations, read things on blogs, read what Bruce Eckel had to say (the first person I saw speak out against them).

I am currently writing some new code and paying very careful attention to how I deal with exceptions. I am trying to see the point of view of the "we don't like checked exceptions" crowd and I still cannot see it.

Every conversation I have ends with the same question going unanswered... let me set it up:

In general (from how Java was designed),

  • 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.

A common argument I hear is that if an exception happens then all the developer is going to do is exit the program.

Another common argument I hear is that checked exceptions make it harder to refactor code.

For the "all I am going to do is exit" argument I say that even if you are exiting you need to display a reasonable error message. If you are just punting on handling errors then your users won't be overly happy when the program exits without a clear indication of why.

For the "it makes it hard to refactor" crowd, that indicates that the proper level of abstraction wasn't chosen. Rather than declare a method throws an IOException, the IOException should be transformed into an exception that is more suited for what is going on.

I don't have an issue with wrapping Main with catch(Exception) (or in some cases catch(Throwable) to ensure that the program can exit gracefully - but I always catch the specific exceptions I need to. Doing that allows me to, at the very least, display an appropriate error message.

The question that people never reply to is this:

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

If the answer is catch Exception then you are also dealing with programmer errors the same way as system exceptions. That seems wrong to me.

If you catch Throwable then you are treating system exceptions and VM errors (and the like) the same way. That seems wrong to me.

If the answer is that you catch only the exceptions you know are thrown then how do you know what ones are thrown? What happens when programmer X throws a new exception and forgot to catch it? That seems very dangerous to me.

I would say that a program that displays a stack trace is wrong. Do people who don't like checked exceptions not feel that way?

So, if you don't like checked exceptions can you explain why not AND answer the question that doesn't get answered please?

Edit: I am not looking for advice on when to use either model, what I am looking for is why people extend from RuntimeException because they don't like extending from Exception and/or why they catch an exception and then rethrow a RuntimeException rather than add throws to their method. I want to understand the motivation for disliking checked exceptions.

30条回答
回忆,回不去的记忆
2楼-- · 2018-12-31 23:47

Exception categories

When talking about exceptions I always refer back to Eric Lippert's Vexing exceptions blog article. He places exceptions into these categories:

  • 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.

An API user:

  • must not handle fatal or boneheaded exceptions.
  • should handle vexing exceptions, but they should not occur in an ideal API.
  • must handle exogenous exceptions.

Checked exceptions

The fact that the API user must handle a particular exception is part of the method's contract between the caller and the callee. The contract specifies, among other things: the number and types of arguments the callee expects, the type of return value the caller can expect, and the exceptions the caller is expected to handle.

Since vexing exceptions should not exist in an API, only these exogenous exceptions must be checked exceptions to be part of the method's contract. Relatively few exceptions are exogenous, so any API should have relatively few checked exceptions.

A checked exception is an exception that must be handled. Handling an exception can be as simple as swallowing it. There! The exception is handled. Period. If the developer wants to handle it that way, fine. But he can't ignore the exception, and has been warned.

API problems

But any API that has checked vexing and fatal exceptions (e.g. the JCL) will put unnecessary strain on the API users. Such exceptions have to be handled, but either the exception is so common that it should not have been an exception in the first place, or nothing can be done when handling it. And this causes Java developers to hate checked exceptions.

Also, many APIs don't have a proper exception class hierarchy, causing all kinds of non-exogenous exception causes to be represented by a single checked exception class (e.g. IOException). And this also causes Java developers to hate checked exceptions.

Conclusion

Exogenous exceptions are those that are not your fault, could not have been prevented, and which should be handled. These form a small subset of all the exceptions that can get thrown. APIs should only have checked exogenous exceptions, and all other exceptions unchecked. This will make better APIs, put less strain on the API user, and therefore reduce the need to catch all, swallow or rethrow unchecked exceptions.

So don't hate Java and its checked exceptions. Instead, hate the APIs that overuse checked exceptions.

查看更多
荒废的爱情
3楼-- · 2018-12-31 23:47

This isn't an argument against the pure concept of checked exceptions, but the class hierarchy Java uses for them is a freak show. We always call the things simply "exceptions" – which is correct, because the language specification calls them that too – but how is an exception named and represented in the type system?

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.

Which of these are the "checked" exceptions? Throwables are checked exceptions, except if they're also Errors, which are unchecked exceptions, and then there's the Exceptions, which are also Throwables and are the main type of checked exception, except there's one exception to that too, which is that if they are also RuntimeExceptions, because that's the other kind of unchecked exception.

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.

Except that even when an exception is checked, there are always (fairly easy) ways to cheat the compiler and throw it without it being checked. If you do you that you may get an UndeclaredThrowableException, except in other cases, where it could throw up as an UnexpectedException, or an UnknownException (which is unrelated to UnknownError, which is only for "serious exceptions"), or an ExecutionException, or an InvocationTargetException, or an ExceptionInInitializerError.

Oh, and we mustn't forget Java 8's snazzy new UncheckedIOException, which is a RuntimeException exception designed to let you throw the exception checking concept out the window by wrapping checked IOException exceptions caused by I/O errors (which don't cause IOError exceptions, although that exists too) that are exceptionally difficult to handle and so you need them to not be checked.

Thanks Java!

查看更多
笑指拈花
4楼-- · 2018-12-31 23:47

We've seen some references to C#'s chief architect.

Here's an alternate point of view from a Java guy about when to use checked exceptions. He acknowledges many of the negatives others have mentioned: Effective Exceptions

查看更多
还给你的自由
5楼-- · 2018-12-31 23:48

Rather than rehash all the (many) reasons against checked exceptions, I'll pick just one. I've lost count of the number of times I've written this block of code:

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

99% of the time I can't do anything about it. Finally blocks do any necessary cleanup (or at least they should).

I've also lost count of the number of times I've seen this:

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

Why? Because someone had to deal with it and was lazy. Was it wrong? Sure. Does it happen? Absolutely. What if this were an unchecked exception instead? The app would've just died (which is preferable to swallowing an exception).

And then we have infuriating code that uses exceptions as a form of flow control, like java.text.Format does. Bzzzt. Wrong. A user putting "abc" into a number field on a form is not an exception.

Ok, i guess that was three reasons.

查看更多
怪性笑人.
6楼-- · 2018-12-31 23:48

I know this is an old question but I've spent a while wrestling with checked exceptions and I've something to add. Please forgive me for the length of it!

My main beef with checked exceptions is that they ruin polymorphism. It's impossible to make them play nicely with polymorphic interfaces.

Take the good ol' Java List interface. We have common in-memory implementations like ArrayList and LinkedList. We also have the the skeletal class AbstractList which makes it easy to design new types of list. For a read-only list we need to implement only two methods: size() and get(int index).

This example WidgetList class reads some fixed-size objects of type Widget (not shown) from a file:

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.

Except it isn't. The above class doesn't compile because the file access methods may throw an IOException, a checked exception which you have to "catch or specify". You can't specify it as thrown -- the compiler won't let you because that would violate the contract of the List interface. And there is no useful way that WidgetList itself can handle the exception (as I'll expound on later).

Apparently the only thing to do is catch and rethrow checked exceptions as some unchecked exception:

@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);
    }
}

((Edit: Java 8 has added an UncheckedIOException class for exactly this case: for catching and rethrowing IOExceptions across polymorphic method boundaries. Kind of proves my point!))

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.

I find that the ubiquitous Runnable interface is often backed into this corner, if it calls something which throws checked exceptions. It can't throw the exception as is, so all it can do is clutter the code by catching and rethrowing as a RuntimeException.

Actually, you can throw undeclared checked exceptions if you resort to hacks. The JVM, at run time, doesn't care about checked exception rules, so we need to fool only the compiler. The easiest way to do this is to abuse generics. This is my method for it (class name shown because (before Java 8) it's required in the calling syntax for the generic method):

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;
    }
}

Hurray! Using this we can throw a checked exception any depth up the stack without declaring it, without wrapping it in a RuntimeException, and without cluttering the stack trace! Using the "WidgetList" example again:

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

Unfortunately, the final insult of checked exceptions is that the compiler refuses to allow you to catch a checked exception if, in its flawed opinion, it could not have been thrown. (Unchecked exceptions do not have this rule.) To catch the sneakily thrown exception we have to do this:

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;
    }
}

That is a bit awkward, but on the plus side, it is still slightly simpler than the code for extracting a checked exception that was wrapped in a RuntimeException.

Happily, the throw t; statement is legal here, even though the type of t is checked, thanks to a rule added in Java 7 about rethrowing caught exceptions.


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.

Except not always. Suppose that Widget has a method for saving it out to a stream:

public void writeTo(OutputStream out) throws IOException;

Declaring this method to accept a plain OutputStream is the right thing to do, so it can be used polymorphically with all kinds of outputs: files, databases, the network, and so on. And in-memory arrays. With an in-memory array, however, there is a spurious requirement to handle an exception that can't actually happen:

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);
}

As usual, checked exceptions get in the way. If your variables are declared as a base type that has more open-ended exception requirements, you have to add handlers for those exceptions even if you know they won't occur in your application.

But wait, checked exceptions are actually so annoying, that they won't even let you do the reverse! Imagine you currently catch any IOException thrown by write calls on an OutputStream, but you want to change the variable's declared type to a ByteArrayOutputStream, the compiler will berate you for trying to catch a checked exception that it says cannot be thrown.

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.


I'm nearly done. My final frustration with checked exceptions is that at most call sites, there's nothing useful you can do with them. Ideally when something goes wrong we'd have a competent application-specific handler that can inform the user of the problem and/or end or retry the operation as appropriate. Only a handler high up the stack can do this because it's the only one that knows the overall goal.

Instead we get the following idiom, which is rampant as a way to shut the compiler up:

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

In a GUI or automated program the printed message won't be seen. Worse, it plows on with the rest of the code after the exception. Is the exception not actually an error? Then don't print it. Otherwise, something else is going to blow up in a moment, by which time the original exception object will be gone. This idiom is no better than BASIC's On Error Resume Next or PHP's error_reporting(0);.

Calling some kind of logger class is not much better:

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

That is just as lazy as e.printStackTrace(); and still plows on with code in an indeterminate state. Plus, the choice of a particular logging system or other handler is application-specific, so this hurts code reuse.

But wait! There is an easy and universal way to find the application-specific handler. It's higher up the call stack (or it is set as the Thread's uncaught exception handler). So in most places, all you need to do is throw the exception higher up the stack. E.g., throw e;. Checked exceptions just get in the way.

I'm sure checked exceptions sounded like a good idea when the language was designed, but in practice I've found them to be all bother and no benefit.

查看更多
低头抚发
7楼-- · 2018-12-31 23:48

I think that this is an excellent question and not at all argumentative. I think that 3rd party libraries should (in general) throw unchecked exceptions. This means that you can isolate your dependencies on the library (i.e. you don't have to either re-throw their exceptions or throw Exception - usually bad practice). Spring's DAO layer is an excellent example of this.

On the other hand, exceptions from the core Java API should in general be checked if they could ever be handled. Take FileNotFoundException or (my favourite) InterruptedException. These conditions should almost always be handled specifically (i.e. your reaction to an InterruptedException is not the same as your reaction to an IllegalArgumentException). The fact that your exceptions are checked forces developers to think about whether a condition is handle-able or not. (That said, I've rarely seen InterruptedException handled properly!)

One more thing - a RuntimeException is not always "where a developer got something wrong". An illegal argument exception is thrown when you try and create an enum using valueOf and there's no enum of that name. This is not necessarily a mistake by the developer!

查看更多
登录 后发表回答