The case against checked exceptions
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),
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.
I think I read the same Bruce Eckel interview that you did - and it's always bugged me. In fact, the argument was made by the interviewee (if this is indeed the post you're talking about) Anders Hejlsberg, the MS genius behind .NET and C#.
http://www.artima.com/intv/handcuffs.html
Fan though I am of Hejlsberg and his work, this argument has always struck me as bogus. It basically boils down to:
"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".
By "otherwise presented to the user" I mean if you use a runtime exception the lazy programmer will just ignore it (versus catching it with an empty catch block) and the user will see it.
The summary of the summary of the argument is that "Programmers won't use them properly and not using them properly is worse than not having them".
There is some truth to this argument and in fact, I suspect Goslings motivation for not putting operator overrides in Java comes from a similar argument - they confuse the programmer because they are often abused.
But in the end, I find it a bogus argument of Hejlsberg's and possibly a post-hoc one created to explain the lack rather than a well thought out decision.
I would argue that while the over-use of checked exceptions is a bad thing and tends to lead to sloppy handling by users, but the proper use of them allows the API programmer to give great benefit to the API client programmer.
Now the API programmer has to be careful not to throw checked exceptions all over the place, or they will simply annoy the client programmer. The very lazy client programmer will resort to catch (Exception) {}
as Hejlsberg warns and all benefit will be lost and hell will ensue. But in some circumstances, there's just no substitute for a good checked exception.
For me, the classic example is the file-open API. Every programming language in the history of languages (on file systems at least) has an API somewhere that lets you open a file. And every client programmer using this API knows that they have to deal with the case that the file they are trying to open doesn't exist. Let me rephrase that: Every client programmer using this API should know that they have to deal with this case. And there's the rub: can the API programmer help them know they should deal with it through commenting alone or can they indeed insist the client deal with it.
In C the idiom goes something like
if (f = fopen("goodluckfindingthisfile")) { ... }
else { // file not found ...
where fopen
indicates failure by returning 0 and C (foolishly) lets you treat 0 as a boolean and... Basically, you learn this idiom and you're okay. But what if you're a noob and you didn't learn the idiom. Then, of course, you start out with
f = fopen("goodluckfindingthisfile");
f.read(); // BANG!
and learn the hard way.
Note that we're only talking about strongly typed languages here: There's a clear idea of what an API is in a strongly typed language: It's a smorgasbord of functionality (methods) for you to use with a clearly defined protocol for each one.
That clearly defined protocol is typically defined by a method signature. Here fopen requires that you pass it a string (or a char* in the case of C). If you give it something else you get a compile-time error. You didn't follow the protocol - you're not using the API properly.
In some (obscure) languages the return type is part of the protocol too. If you try to call the equivalent of fopen()
in some languages without assigning it to a variable you'll also get a compile-time error (you can only do that with void functions).
The point I'm trying to make is that: In a statically typed language the API programmer encourages the client to use the API properly by preventing their client code from compiling if it makes any obvious mistakes.
(In a dynamically typed language, like Ruby, you can pass anything, say a float, as the file name - and it will compile. Why hassle the user with checked exceptions if you're not even going to control the method arguments. The arguments made here apply to statically-typed languages only.)
So, what about checked exceptions?
Well here's one of the Java APIs you can use for opening a file.
try {
f = new FileInputStream("goodluckfindingthisfile");
}
catch (FileNotFoundException e) {
// deal with it. No really, deal with it!
... // this is me dealing with it
}
See that catch? Here's the signature for that API method:
public FileInputStream(String name)
throws FileNotFoundException
Note that FileNotFoundException
is a checked exception.
The API programmer is saying this to you: "You may use this constructor to create a new FileInputStream but you
a) must pass in the file name as a String
b) must accept the possibility that the file might not be found at runtime"
And that's the whole point as far as I'm concerned.
The key is basically what the question states as "Things that are out of the programmer's control". My first thought was that he/she means things that are out of the API programmers control. But in fact, checked exceptions when used properly should really be for things that are out of both the client programmer's and the API programmer's control. I think this is the key to not abusing checked exceptions.
I think the file-open illustrates the point nicely. The API programmer knows you might give them a file name that turns out to be nonexistent at the time the API is called, and that they won't be able to return you what you wanted, but will have to throw an exception. They also know that this will happen pretty regularly and that the client programmer might expect the file name to be correct at the time they wrote the call, but it might be wrong at runtime for reasons beyond their control too.
So the API makes it explicit: There will be cases where this file doesn't exist at the time you call me and you had damn well better deal with it.
This would be clearer with a counter-case. Imagine I'm writing a table API. I have the table model somewhere with an API including this method:
public RowData getRowData(int row)
Now as an API programmer I know there will be cases where some client passes in a negative value for the row or a row value outside of the table. So I might be tempted to throw a checked exception and force the client to deal with it:
public RowData getRowData(int row) throws CheckedInvalidRowNumberException
(I wouldn't really call it "Checked" of course.)
This is bad use of checked exceptions. The client code is going to be full of calls to fetch row data, every one of which is going to have to use a try/catch, and for what? Are they going to report to the user that the wrong row was sought? Probably not - because whatever the UI surrounding my table view is, it shouldn't let the user get into a state where an illegal row is being requested. So it's a bug on the part of the client programmer.
The API programmer can still predict that the client will code such bugs and should handle it with a runtime exception like an IllegalArgumentException
.
With a checked exception in getRowData
, this is clearly a case that's going to lead to Hejlsberg's lazy programmer simply adding empty catches. When that happens, the illegal row values will not be obvious even to the tester or the client developer debugging, rather they'll lead to knock-on errors that are hard to pinpoint the source of. Arianne rockets will blow up after launch.
Okay, so here's the problem: I'm saying that the checked exception FileNotFoundException
is not just a good thing but an essential tool in the API programmers toolbox for defining the API in the most useful way for the client programmer. But the CheckedInvalidRowNumberException
is a big inconvenience, leading to bad programming and should be avoided. But how to tell the difference.
I guess it's not an exact science and I guess that underlies and perhaps justifies to a certain extent Hejlsberg's argument. But I'm not happy throwing the baby out with the bathwater here, so allow me to extract some rules here to distinguish good checked exceptions from bad:
Out of client's control or Closed vs Open:
Checked exceptions should only be used where the error case is out of control of both the API and the client programmer. This has to do with how open or closed the system is. In a constrained UI where the client programmer has control, say, over all of the buttons, keyboard commands etc that add and delete rows from the table view (a closed system), it is a client programming bug if it attempts to fetch data from a nonexistent row. In a file-based operating system where any number of users/applications can add and delete files (an open system), it is conceivable that the file the client is requesting has been deleted without their knowledge so they should be expected to deal with it.
Ubiquity:
Checked exceptions should not be used on an API call that is made frequently by the client. By frequently I mean from a lot of places in the client code - not frequently in time. So a client code doesn't tend to try to open the same file a lot, but my table view gets RowData
all over the place from different methods. In particular, I'm going to be writing a lot of code like
if (model.getRowData().getCell(0).isEmpty())
and it will be painful to have to wrap in try/catch every time.
Informing the User:
Checked exceptions should be used in cases where you can imagine a useful error message being presented to the end user. This is the "and what will you do when it happens?" question I raised above. It also relates to item 1. Since you can predict that something outside of your client-API system might cause the file to not be there, you can reasonably tell the user about it:
"Error: could not find the file 'goodluckfindingthisfile'"
Since your illegal row number was caused by an internal bug and through no fault of the user, there's really no useful information you can give them. If your app doesn't let runtime exceptions fall through to the console it will probably end up giving them some ugly message like:
"Internal error occured: IllegalArgumentException in ...."
In short, if you don't think your client programmer can explain your exception in a way that helps the user, then you should probably not be using a checked exception.
So those are my rules. Somewhat contrived, and there will doubtless be exceptions (please help me refine them if you will). But my main argument is that there are cases like FileNotFoundException
where the checked exception is as important and useful a part of the API contract as the parameter types. So we should not dispense with it just because it is misused.
Sorry, didn't mean to make this so long and waffly. Let me finish with two suggestions:
A: API programmers: use checked exceptions sparingly to preserve their usefulness. When in doubt use an unchecked exception.
B: Client programmers: get in the habit of creating a wrapped exception (google it) early on in your development. JDK 1.4 and later provide a constructor in RuntimeException
for this, but you can easily create your own too. Here's the constructor:
public RuntimeException(Throwable cause)
Then get in the habit of whenever you have to handle a checked exception and you're feeling lazy (or you think the API programmer was overzealous in using the checked exception in the first place), don't just swallow the exception, wrap it and rethrow it.
try {
overzealousAPI(thisArgumentWontWork);
}
catch (OverzealousCheckedException exception) {
throw new RuntimeException(exception);
}
Put this in one of your IDE's little code templates and use it when you're feeling lazy. This way if you really need to handle the checked exception you'll be forced to come back and deal with it after seeing the problem at runtime. Because, believe me (and Anders Hejlsberg), you're never going to come back to that TODO in your
catch (Exception e) { /* TODO deal with this at some point (yeah right) */}
The thing about checked exceptions is that they are not really exceptions by the usual understanding of the concept. Instead, they are API alternative return values.
The whole idea of exceptions is that an error thrown somewhere way down the call chain can bubble up and be handled by code somewhere further up, without the intervening code having to worry about it. Checked exceptions, on the other hand, require every level of code between the thrower and the catcher to declare they know about all forms of exception that can go through them. This is really little different in practice to if checked exceptions were simply special return values which the caller had to check for. eg.[pseudocode]:
public [int or IOException] writeToStream(OutputStream stream) {
[void or IOException] a= stream.write(mybytes);
if (a instanceof IOException)
return a;
return mybytes.length;
}
Since Java can't do alternative return values, or simple inline tuples as return values, checked exceptions are are a reasonable response.
The problem is that a lot of code, including great swathes of the standard library, misuse checked exceptions for real exceptional conditions that you might very well want to catch several levels up. Why is IOException not a RuntimeException? In every other language I can let an IO exception happen, and if I do nothing to handle it, my application will stop and I'll get a handy stack trace to look at. This is the best thing that can happen.
Maybe two methods up from the example you want to catch all IOExceptions from the whole writing-to-stream process, abort the process and jump into the error reporting code; in Java you can't do that without adding 'throws IOException' at every call level, even levels that themselves do no IO. Such methods should not need to know about the exception handling; having to add exceptions to their signatures:
And then there's plenty of just ridiculous library exceptions like:
try {
httpconn.setRequestMethod("POST");
} catch (ProtocolException e) {
throw new CanNeverHappenException("oh dear!");
}
When you have to clutter up your code with ludicrous crud like this, it is no wonder checked exceptions receive a bunch of hate, even though really this is just simple poor API design.
Another particular bad effect is on Inversion of Control, where component A supplies a callback to generic component B. Component A wants to be able to let an exception throw from its callback back to the place where it called component B, but it can't because that would change the callback interface which is fixed by B. A can only do it by wrapping the real exception in a RuntimeException, which is yet more exception-handling boilerplate to write.
Checked exceptions as implemented in Java and its standard library mean boilerplate, boilerplate, boilerplate. In an already verbose language this is not a win.
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.
链接地址: http://www.djcxy.com/p/25878.html上一篇: 在WPF应用程序中全局捕获异常?
下一篇: 针对检查异常的情况