简体   繁体   中英

Java Exceptions Hierarchy, what is the point?

I'm finding myself busy refactoring my pet project and removing all the noise associated with exception throwing declarations. Basically, everything that violates a condition is assertion violation or, formally, AssertionError, which in Java is generously allowed to be omitted from method's signature. My question is: what is the point of having Exceptions hierarchy? My experience is that each and every exception is unique, and there is no formal criteria to establish that one set of exceptions is a subset of the other. Even distinction between checked and unchecked exceptions is fuzzy, why, for example, would I insist on the client code catching exception when a lazy (or impatient) programmer can easily wrap it into RuntimeException and rethrow it on me?

In theory the Java exception hierarchy makes a certain amount of sense:

Throwable*
 -> Error (OutOfMemoryError, etc.)
 -> Exception (IOException, SQLException, etc.)
     -> RuntimeException (IndexOutOfBoundsException, NullPointer, etc.)

Now the theory behind these actually makes a certain amount of sense. (The actual implementation leaves something to be desired because of accumulated cruft, sadly.)

Error -descended Throwable objects are serious errors that a program is not expected to be able to recover from. (You generally don't catch these, in other words.) One of these popping up is a serious overall system problem. For example when you run out of memory, this represents a serious failing somewhere since in theory the GC has by now desperately tried to free space for you. Catching this is pointless.

Exception -descended Throwable objects are all errors that the program can reasonably be expected to encounter during normal operations; things like network errors, file system errors, etc. Indeed with the exception of those descended from RuntimeException , it is mandated that programs explicitly handle these errors – they're the so-called "checked exceptions". (Of course bad programmers will "handle" these by stubbing them out, but that's a programmer problem, not a system problem.)

RuntimeException -descended Throwable objects are slightly different. They are errors which should not necessarily be expected but which a program could reasonably recover from when they occur. As such they are not checked (programs are not obligated to handle these) but they may if there are reasonable ways to handle the situation. In general these exceptions represent programming errors of some sort (as opposed to the previous category which are expected errors which occur in normal operation).

So does this hierarchy make sense? Well, at some level it seems to. Error is thrown by the system and represents a major failure that's probably going to kill your program. RuntimeException , when used properly, is thrown by the system libraries (or occasionally by your own program) and generally means someone screwed up somewhere, but it's OK because you might be able to recover from it. Other Exception objects are expected errors that are actually part of the stated interface of your objects.

But...

That last item is the problem. Checked exceptions are, to put it bluntly, a serious pain in the lower reaches of the torso's anatomy. They needlessly clutter up the code with exception handling boilerplate in such a way as to render, in my opinion (and many others' I might add!), the whole point of exceptions moot: separation of error detection and error handling. By forcing every method in the chain to handle the exception—even if it's just to rewrap it and pass it on!—the code gets cluttered with minutiae of error handling to the point that it is little better than returning status codes and handling them after each method call.

Were Java a smarter programming language the checked exceptions would be checked at compile/link time to see if they were properly handled system-wide, not at each and every method call in each and every class file. Unfortunately Java's entire architecture doesn't permit this level of whole-program analysis and the result is, again in my opinion (but again shared by many), actually a blend of the worst of the two worlds of exception handling and error returns: you get most of the boilerplate scaffolding of explicit error returns but you also get the COME FROM -like behaviour of exceptions.

"My experience is that each and every exception is unique, and there is no formal criteria to establish that one set of exceptions is a subset of the other. Even distinction between checked and unchecked exceptions is fuzzy, why, for example, would I insist on the client code catching exception when a lazy (or impatient) programmer can easily wrap it into RuntimeException and rethrow it on me?"

All of that is correct.

Like Joel Spolsky once put it : "design is the art of making choices". And thus, designing an exceptions hierarchy involves making choices. Sometimes they work out pretty well, sometimes they don't.

One advantage [of having exceptions hierarchies] I see is that by catching a supertype-exception, you catch a whole host of exception types with a single catch clause. Is that always what the user wants ? Definitely not. Can the user escape from it in those (and only those) cases where it is not what he wants ? Definitely yes. Is the alternative of not having supertype-exceptions at all better ? Imo definitely not. Take a look at the IOException hierarchy and imagine having to write a catch clause for each and every one of its children, in each and every place where they can arise ...

Even with all the arguable disadvantages that are, by this time, pretty well known, I still believe Java's exception handling mechanism beats any other I've ever seen.

I don't think the hierarchy is bad, it just isn't too useful. 99% of the time, an exception signals that something very bad has happened and my options are few. We're basically dead. The only choice is the selection of an appropriate error message to display to the users.

I'm one of those lazy programmers. If I call a 'processFile()' method from the bowels of my code and it throws an exception, I'll probably formulate a "Your file could not be processed" message and rethrow a RuntimeException. We can't recover. We are an ex-program. We Are No More. There's nothing to be gained in junking up the code by tacking a checked exception to every method in the call stack.

Invariably, I code something like this:

try {
    processAFile();
} catch (Exception e) { // Just catch them all.
    logger.error(e);
    logger.error("log any important information here.");
    throw new RuntimeException("We were unable to process your file.");
}

Now the RuntimeException rattles all the way to the main method and is handled responsibly.

At the top of the code, I catch all exceptions, log as appropriate, generally roll back the transaction, and do what's needed to display the customer-friendly error message.

I like the distinction between checked and unchecked exceptions though I can't say why as I think about it. IntelliJ will automatically fill in several exception catch blocks. I think 'hmm that's interesting' and replace them with a single catch (Exception e) since the recovery is always the same. In the example above I have to catch checked and unchecked, then I'm dead and the error message is the same so who cares. Why throw a checked exception. Dead is dead.

The only time I can think of where I handle specific exceptions is when I call a method that improperly throws an exception instead of returning an error condition.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM