Does Java need Checked Exceptions?

Although C++ introduced the exception specification, Java is the only mainstream language that enforces the use of specifications with "Checked Exceptions." In this discussion I will examine the motivation and outcome of this experiment, and look at alternative (and possibly more useful) ways to manage exceptions. The goal of this discussion is to explore the ideas -- in particular, what are your experiences with exceptions? Would it be more beneficial to you to use un-checked exceptions?
Note: You can find an associated essay
here, and a related article here.

I began learning about exceptions when they were introduced into the C++ committee, and it's been a long learning curve. One of the first justifications for exceptions was that they would allow programmers to write less error checking code because this code could be delayed until a more appropriate point in the program, rather than having to put tests at the point of every function call -- which no one was doing anyway. In fact, I think it was the poor error-handling model that C brought in that was the major motivation for exception handling in C++, because what we really needed was a unified and consistent way of reporting errors (unfortunately, because C++ is backwards-compatible with C, exception handling in C++ is simply an additional error handling model).

Checked exceptions seem like a really good idea at first. But it's all based on our unchallenged assumption that static type checking detects your problems and is always best. Java is the first language (that I know of) that uses checked exceptions and is thus the first experiment. However, the kind of code you must write around these things and the common phenomenon of "swallowed" exceptions begins to suggest there's a problem. In Python, exceptions are unchecked, and you can catch them and do something if you want, but you aren't forced. And it seems to work just fine.

I think it's a compile-time vs. run-time checking issue. We have gotten so used to thinking that the only correct way, only safe and reliable way, to do things is at compile time, that we automatically discount any solutions that rely on run-time as unreliable. That thinking came from C++, but I note that Java actually does a fair number of things at runtime, which we accept merely because they can't be done at compile time. But despite that we still hold this idea that if it can be done at compile time, then that's the only proper time to do it (I am also referring here to weak typing in Python). I know this seems like a less precise and provable way of thinking, but if you start having experiences that seem to disprove the common way of thinking then you start questioning it.

I began having discussions about this last Summer, and which I've started hearing from other people about -- that checked exceptions were a mistake. They're not in Python or C#, or C++. In fact, the only language I know of where they exist is in Java, and I'll bet it was because people saw unchecked exception specifications in C++ and thought that was a mistake (I know I did, for the longest time). At this point, I feel like checked exceptions are (1) an untried experiment when they were put into Java (unless you know of some other language where it is implemented ... Ada, perhaps?) (2) a failure because so many people end up swallowing the exceptions in their code.

In the Python/C# approach, the exception is thrown, and if you want to you can write code to catch it, but if you don't you aren't forced to write a bunch of extra code and be tempted to swallow the exception.

I currently plan to rewrite the Exceptions chapter (and the rest of the book) for the 3rd edition to change the way exceptions are handled.

The way I (now) see exceptions is something like this:

1) The great value of exceptions is the unification of error reporting: a standard mechanism by which to report errors, rather than the potpourri of ignorable approaches that we had in C (and thus, C++, which only adds exceptions to the mix, and doesn't make it the exclusive approach). The big advantage Java has over C++ is that exceptions are the only way to report errors.

2) "Ignorable" in the previous paragraph is the other issue. The theory is that if the compiler forces the programmer to either handle the exception or pass it on in an exception specification, then the programmer's attention will always be brought back to the possibility of errors and they will thus properly take care of them. I think the problem is that this is an untested assumption we're making as language designers that falls into the field of psychology. My theory is that when someone is trying to do something and you are constantly prodding them with annoyances, they will use the quickest device available to make those annoyances go away so they can get their thing done, perhaps assuming they'll go back and take out the device later. I discovered I had done this in the first edition of Thinking in Java:

 

...
} catch (SomeKindOfException e) {}

 
And then more or less forgot it until the rewrite. How many people thought this was a good example and followed it? I began seeing the same kind of code, and realized people were stubbing out exceptions and then they were disappearing. The overhead of checked exceptions was having the opposite effect of what was intended, something that can happen when you experiment (and I now believe that checked exceptions were an experiment based on what someone thought was a good idea, and which I believed was a good idea until recently).

When I started using Python, all the exceptions appeared, none were accidentally "disappeared." If you want to catch an exception, you can, but you aren't forced to write reams of code all the time just to be passing the exceptions around. They go up to where you want to catch them, or they go all the way out if you forget (and thus they remind you) but they don't vanish, which is the worst of all possible cases. I now believe that checked exceptions encourage people to make them vanish. Plus they make much less readable code.

In the end, I think we must realize the experimental nature of checked exceptions and look at them carefully before assuming that everything about exceptions in Java is good. I believe that having a single mechanism for handling errors is excellent, and I believe that using a separate channel (the exception handling mechanism) for moving the exceptions around is good. But I do remember one of the early arguments for exception handling in C++ was that it would allow the programmer to separate the sections of code where you just wanted to get work done from the sections where you handled errors, and it seems to me that checked exceptions do not do this; instead, they tend to intrude (a lot) into your "normal working code" and thus are a step backwards. My experience with Python exceptions supports this, and unless I get turned around on this issue I intend to put a lot more RuntimeExceptions into my Java code.

One thing has become very clear to me, especially because of Python: the more random rules you pile onto the programmer, rules that have nothing to do with solving the problem at hand, the slower the programmer can produce. And this does not appear to be a linear factor, but an exponential one.

I've gotten a report or two where people were saying that checked exceptions were such a problem (getting swallowed) in production code that they wanted to change the situation. It may be that the majority of programmers out there are what you might classify as beginners. I've seen this again and again in seminars -- people with years of programming experience who don't understand some basic things.

Maybe it's a time thing. I started struggling with the idea of exceptions when they were introduced at the C++ committee, and after this much time it suddenly hit me. But I also suspect it comes from using a language that has exceptions, but not checked exceptions. I think it's the best of both worlds -- if I want to catch the exception, I can, but I'm not tempted to swallow it just to avoid writing reams of code. If I don't' want to write around the exceptions, I ignore them, and if one comes up it gets reported to me during debugging, and I can decide how to handle it then. I still deal with the exception, but I'm not forced to write a bunch of code about exceptions all the time. ExceptionAdapter

Here's a tool that I developed with the help of Heinz Kabutz. It converts any checked exception into a RuntimeException while preserving all the information from the checked exception. 

 

import java.io.*;
class ExceptionAdapter extends RuntimeException {
  private final String stackTrace;
  public Exception originalException;
  public ExceptionAdapter(Exception e) {
    super(e.toString());
    originalException = e;
    StringWriter sw = new StringWriter();
    e.printStackTrace(new PrintWriter(sw));
    stackTrace = sw.toString();
  }
  public void printStackTrace() { 
    printStackTrace(System.err);
  }
  public void printStackTrace(java.io.PrintStream s) { 
    synchronized(s) {
      s.print(getClass().getName() + ": ");
      s.print(stackTrace);
    }
  }
  public void printStackTrace(java.io.PrintWriter s) { 
    synchronized(s) {
      s.print(getClass().getName() + ": ");
      s.print(stackTrace);
    }
  }
  public void rethrow() { throw originalException; }
} 

 
 

  

The original exception is stored in originalException, so you can always recover it. In addition, its stack trace information is extracted into the stackTrace string, which will then be printed using the usual printStackTrace() if the exception gets all the way out to the console. However, you can also put a catch clause at a higher level in your program to catch an ExceptionAdapter and look for particular types of exceptions, like this:

 

catch(ExceptionAdapter ea) {
  try {
    ea.rethrow();
  } catch(IllegalArgumentException e) {
    // ...
  } catch(FileNotFoundException e) {
    // ...
  }
  // etc.
}

 

 

 

 

 Here, you're still able to catch the specific type of exception but you're not forced to put in all the exception specifications and try-catch clauses everywhere between the origin of the exception and the place that it's caught. An even more importantly, no one writing code is tempted to swallow the exception and thus erase it. If you forget to catch some exception, it will show up at the top level. If you want to catch exceptions somewhere in between, you can.

Or, since originalException is public, you can also use RTTI to look for particular types of exceptions.

Here's some test code, just to make sure it works (not the way I suggest using it, however):

public class ExceptionAdapterTest {
  public static void main(String[] args) {
    try {
      try {
        throw new java.io.FileNotFoundException("Bla");
      } catch(Exception ex) {
        ex.printStackTrace();
        throw new ExceptionAdapter(ex);
      }   
    } catch(RuntimeException e) {
      e.printStackTrace();
    }
    System.out.println("That's all!");
  }
}

 

 

By using this tool you can get the benefits of the unchecked exception approach (less code, cleaner code) without losing the core of the information about the exception.

If you were writing code where you wanted to throw a particular type of checked exception, you could use (or modify, if it isn't already possible) the ExceptionAdapter like this:

 

 

if(futzedUp)
    throw new ExceptionAdapter(new CloneNotSupportedException());

 

 

This means you can easily use all the exceptions in their original role, but with unchecked-style coding.

 

Kevlin Henney writes:

I must admit that I've come to similar conclusions myself based on experience in Java and C++, and readings in other languages. My only caveat on it is that I have found exception specifications, in the style of CORBA, to be useful where errors are propagated across significant boundaries, ie machine boundaries, and where such interfaces need to be more explicit. However, although apparently similar to Java's EH mechanism, there is no concept of compile-time checking.

To clarify a couple of points in your article, Ada does not have any form of exception specification, and so does not have a checked/unchecked model. The roots of exception specifications go back to CLU and further. The key paper for this is "Exception Handling in CLU" by Barbara Liskov and Alan Snyder (IEEE Transactions on Software Engineering, Vol SE-5, No 6, Nov 1979). Unfortunately I cannot find a copy of this paper online, and have only a paper copy. There is a "History of CLU" paper here.

Unfortunately, it does not provide much detail on the exception handling mechanism. What is worth noting is that information-rich exceptions are seen as an extension of the procedural paradigm (and are therefore not necessarily an object-related concept), that CLU supported the equivalent of throw specs in its procedure signatures, that there were no compile-time checks of procedure body against the signature (a conscious design decision to avoid overwhelming programmers with irrelevant detail :->), and that unlisted exceptions automatically translated to a special failure exception.

In this you can see the origins of C++'s mechanisms, and the screws that were tightened -- interestingly rejecting the original rationale for not having compile-time checks -- in Java. The CLU mechanism has been influential elsewhere, perhaps the closest offspring being in Modula-3 - - the language report is available here

Interestingly, Modula-3 also chooses to have a throw spec that is runtime rather than compile-time checked.

Kevlin Henney
http://www.curbralan.com


 

Here's an interesting comment by one of the C# language designers: (Full comment). Note in particular:

Examination of small programs leads to the conclusion that requiring exception specifications could both enhance developer productivity and enhance code quality, but experience with large software projects suggests a different result -- decreased productivity and little or no increase in code quality.

The rest of the note goes on to argue this claim. The reason I find this particularly compelling is that it does agree that checked exceptions seem to be helpful for small projects, which is generally the space where we argue the point. However, when projects get large (actually, I've noticed it when they are anything except small), checked exceptions get ungainly and seem to cause problems. I would therefore suggest that the reason checked exceptions seem so compellingly "right" at first is that they have been presented and argued in the realm of small examples.

你可能感兴趣的:(java,C++,c,python,C#)