可以将文章内容翻译成中文,广告屏蔽插件可能会导致该功能失效(如失效,请关闭广告屏蔽插件后再试):
问题:
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.
回答1:
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) */}
回答2:
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:
- unnecessarily increases coupling;
- makes interface signatures very brittle to change;
- makes the code less readable;
- is so annoying that it the common programmer reaction is to defeat the system by doing something horrible like ‘throws Exception’, ‘catch (Exception e) {}’, or wrapping everything in a RuntimeException (which makes debugging harder).
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.
回答3:
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.
回答4:
Well, it\'s not about displaying a stacktrace or silently crashing. It\'s about being able to communicate errors between layers.
The problem with checked exceptions is they encourage people to swallow important details (namely, the exception class). If you choose not to swallow that detail, then you have to keep adding throws declarations across your whole app. This means 1) that a new exception type will affect lots of function signatures, and 2) you can miss a specific instance of the exception you actually -want- to catch (say you open a secondary file for a function that writes data to a file. The secondary file is optional, so you can ignore its errors, but because the signature throws IOException
, it\'s easy to overlook this).
I\'m actually dealing with this situation now in an application. We repackaged almost exceptions as AppSpecificException. This made signatures really clean and we didn\'t have to worry about exploding throws
in signatures.
Of course, now we need to specialize the error handling at the higher levels, implementing retry logic and such. Everything is AppSpecificException, though, so we can\'t say \"If an IOException is thrown, retry\" or \"If ClassNotFound is thrown, abort completely\". We don\'t have a reliable way of getting to the real exception because things get repackaged again and again as they pass between our code and third-party code.
This is why I\'m a big fan of the exception handling in python. You can catch only the things you want and/or can handle. Everything else bubbles up as if you rethrew it yourself (which you have done anyways).
I\'ve found, time and time again, and throughout the project I mentioned, that exception handling falls into 3 categories:
- Catch and handle a specific exception. This is to implement retry logic, for example.
- Catch and rethrow other exceptions. All that happens here is usually logging, and its usually a trite message like \"Unable to open $filename\". These are errors you can\'t do anything about; only a higher levels knows enough to handle it.
- Catch everything and display an error message. This is usually at the very root of a dispatcher, and all it does it make sure it can communicate the error to the caller via a non-Exception mechanism (popup dialogue, marshaling an RPC Error object, etc).
回答5:
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 IOException
s 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 IOException
s, 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.
回答6:
SNR
Firstly, checked exceptions decrease the \"signal-to-noise ratio\" for the code. Anders Hejlsberg also talks about imperative vs declarative programming which is a similar concept. Anyway consider the following code snippets:
Update UI from non UI-thread in Java:
try {
// Run the update code on the Swing thread
SwingUtilities.invokeAndWait(() -> {
try {
// Update UI value from the file system data
FileUtility f = new FileUtility();
uiComponent.setValue(f.readSomething());
} catch (IOException e) {
throw new UncheckedIOException(e);
}
});
} catch (InterruptedException ex) {
throw new IllegalStateException(\"Interrupted updating UI\", ex);
} catch (InvocationTargetException ex) {
throw new IllegalStateException(\"Invocation target exception updating UI\", ex);
}
Update UI from non UI-thread in C#:
private void UpdateValue()
{
// Ensure the update happens on the UI thread
if (InvokeRequired)
{
Invoke(new MethodInvoker(UpdateValue));
}
else
{
// Update UI value from the file system data
FileUtility f = new FileUtility();
uiComponent.Value = f.ReadSomething();
}
}
Which seems a lot clearer to me. When you start to do more and more UI work in Swing checked exceptions start to become really annoying and useless.
Jail Break
To implement even the most basic of implementations, such as Java\'s List interface, checked exceptions as a tool for design by contract fall down. Consider a list that is backed by a database or a filesystem or any other implementation that throws a checked exception. The only possible implementation is to catch the checked exception and rethrow it as an unchecked exception:
@Override
public void clear()
{
try
{
backingImplementation.clear();
}
catch (CheckedBackingImplException ex)
{
throw new IllegalStateException(\"Error clearing underlying list.\", ex);
}
}
And now you have to ask what is the point of all that code? The checked exceptions just add noise, the exception has been caught but not handled and design by contract (in terms of checked exceptions) has broken down.
Conclusion
- Catching exceptions is different to handling them.
- Checked exceptions add noise to the code.
- Exception handling works well in C# without them.
I blogged about this previously.
回答7:
Artima published an interview with one of the architects of .NET, Anders Hejlsberg, which acutely covers the arguments against checked exceptions. A short taster:
The throws clause, at least the way it\'s implemented in Java, doesn\'t necessarily force you to handle the exceptions, but if you don\'t handle them, it forces you to acknowledge precisely which exceptions might pass through. It requires you to either catch declared exceptions or put them in your own throws clause. To work around this requirement, people do ridiculous things. For example, they decorate every method with, \"throws Exception.\" That just completely defeats the feature, and you just made the programmer write more gobbledy gunk. That doesn\'t help anybody.
回答8:
The article Effective Java Exceptions explains nicely when to use unchecked and when to use checked exceptions. Here are some quotes from that article to highlight the main points:
Contingency:
An expected condition demanding an alternative response from a method that can be expressed in terms of the method\'s intended purpose. The caller of the method expects these kinds of conditions and has a strategy for coping with them.
Fault:
An unplanned condition that prevents a method from achieving its intended purpose that cannot be described without reference to the method\'s internal implementation.
(SO doesn\'t allow tables, so you might want to read the following from the original page...)
Contingency
- Is considered to be: A part of the design
- Is expected to happen: Regularly but rarely
- Who cares about it: The upstream code that invokes the method
- Examples: Alternative return modes
- Best Mapping: A checked exception
Fault
- Is considered to be: A nasty surprise
- Is expected to happen: Never
- Who cares about it: The people who need to fix the problem
- Examples: Programming bugs, hardware malfunctions, configuration mistakes,
missing files, unavailable servers
- Best Mapping: An unchecked exception
回答9:
I initially agreed with you, as I\'ve always been in favour of checked exceptions, and began to think about why I don\'t like not having checked exceptions in .Net. But then I realised that I don\'t infact like checked exceptions.
To answer you question, yes, I like my programs to show stack traces, preferably really ugly ones. I want the application to explode into a horrible heap of the ugliest error messages you could ever want to see.
And the reason is because, if it does that, I have to fix it, and I have to fix it right away. I want to know immediately that there is a problem.
How many times do you actually handle exceptions? I\'m not talking about catching exceptions -- I\'m talking about handling them? It\'s too easy to write the following:
try {
thirdPartyMethod();
} catch(TPException e) {
// this should never happen
}
And I know you can say that it\'s bad practice, and that \'the answer\' is to do something with the exception (let me guess, log it?), but in the Real World (tm), most programmers just don\'t do it.
So yes, I don\'t want to catch exceptions if I don\'t have to do so, and I want my program to blow up spectacularly when I screw up. Silently failing is the worst possible outcome.
回答10:
In short:
Exceptions are an API design question. -- No more, no less.
The argument for checked exceptions:
To understand why checked exceptions might not be good thing, let\'s turn the question around and ask: When or why are checked exceptions attractive, i.e. why would you want the compiler to enforce declaration of exceptions?
The answer is obvious: Sometimes you need to catch an exception, and that is only possible if the code being called offers a specific exception class for the error that you are interested in.
Hence, the argument for checked exceptions is that the compiler forces programmers to declare which exceptions are thrown, and hopefully the programmer will then also document specific exception classes and the errors that cause them.
In reality though, ever too often a package com.acme
only throws an AcmeException
rather than specific subclasses. Callers then need to handle, declare, or re-signal AcmeExceptions
, but still cannot be certain whether an AcmeFileNotFoundError
happened or an AcmePermissionDeniedError
.
So if you\'re only interested in an AcmeFileNotFoundError
, the solution is to file a feature request with the ACME programmers and tell them to implement, declare, and document that subclass of AcmeException
.
So why bother?
Hence, even with checked exceptions, the compiler cannot force programmers to throw useful exceptions. It is still just a question of the API\'s quality.
As a result, languages without checked exceptions usually do not fare much worse. Programmers might be tempted to throw unspecific instances of a general Error
class rather than an AcmeException
, but if they care at all about their API quality, they will learn to introduce an AcmeFileNotFoundError
after all.
Overall, the specification and documentation of exceptions is not much different from the specification and documentation of, say, ordinary methods. Those, too, are an API design question, and if a programmer forgot to implement or export a useful feature, the API needs to be improved so that you can work with it usefully.
If you follow this line of reasoning, it should be obvious that the \"hassle\" of declaring, catching, and re-throwing of exceptions that is so common in languages like Java often adds little value.
It is also worth noting that the Java VM does not have checked exceptions -- only the Java compiler checks them, and class files with changed exception declarations are compatible at run time. Java VM security is not improved by checked exceptions, only coding style.
回答11:
I have been working with several developers in the last three years in relatively complex applications. We have a code base that uses Checked Exceptions quite often with proper error handling, and some other that doesn\'t.
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)
When I work with the codebase that does not have checked exceptions, it makes it to me a little bit harder to know before hand what can I expect when calling the function, which can break some stuff terribly.
This is all of course a matter of preference and developer skill. Both ways of programming and error handling can be equally effective (or noneffective), so I wouldn\'t say that there is The One Way.
All in all, I find it easier to work with Checked Exceptions, specially in large projects with lot of developers.
回答12:
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.
回答13:
Ok... Checked exceptions are not ideal and have some caveat but they do serve a purpose. When creating an API there are specific cases of failures that are contractual of this API. When in the context of a strongly statically typed language such as Java if one does not use checked exceptions then one must rely on ad-hoc documentation and convention to convey the possibility of error. Doing so removes all benefit that the compiler can bring in handling error and you are left completely to the good will of programmers.
So, one removes Checked exception, such as was done in C#, how then can one programmatically and structurally convey the possibility of error ? How to inform the client code that such and such errors can occur and must be dealt with ?
I hear all sorts of horrors when dealing with checked exceptions, they are misused this is certain but so are unchecked exceptions. I say wait a few years when APIs are stacked many layers deep and you will be begging for the return of some kind of structured mean to convey failures.
Take the case when the exception was thrown somewhere at the bottom of the API layers and just bubbled up because nobody knew it was even possible for this error to occur, this even though it was a type of error that was very plausible when the calling code threw it (FileNotFoundException for example as opposed to VogonsTrashingEarthExcept... in which case it would not matter if we handle it or not since there is nothing left to handle it with).
Many have argued that not being able to load the file was almost always the end of the world for the process and it must die a horrible and painful death. So yeah.. sure ... ok.. you build an API for something and it loads file at some point... I as the user of said API can only respond... \"Who the hell are you to decide when my program should crash !\" Sure Given the choice where exceptions are gobbled up and leave no trace or the EletroFlabbingChunkFluxManifoldChuggingException with a stack trace deeper than the Marianna trench I would take the latter without a cinch of hesitation, but does this mean that it is the desirable way to deal with exception ? Can we not be somewhere in the middle, where the exception would be recast and wrapped each time it traversed into a new level of abstraction so that it actually means something ?
Lastly, most of the argument I see is \"I don\'t want to deal with exceptions, many people do not want to deal with exceptions. Checked exceptions force me to deal with them thus I hate checked exception\" To eliminate such mechanism altogether and relegate it to the chasm of goto hell is just silly and lacks jugement and vision.
If we eliminate checked exception we could also eliminate the return type for functions and always return a \"anytype\" variable... That would make life so much simpler now would it not ?
回答14:
Indeed, checked exceptions on the one hand increase robustness and correctness of your program (you\'re forced to make correct declarations of your interfaces -the exceptions a method throws are basically a special return type). On the other hand you face the problem that, since exceptions \"bubble up\", very often you need to change a whole lot of methods (all the callers, and the callers of the callers, and so on) when you change the exceptions one method throws.
Checked exceptions in Java do not solve the latter problem; C# and VB.NET throw out the baby with the bathwater.
A nice approach that takes the middle road is described in this OOPSLA 2005 paper (or the related technical report.)
In short, it allows you to say: method g(x) throws like f(x)
, which means that g throws all the exceptions f throws. Voila, checked exceptions without the cascading changes problem.
Although it is an academic paper, I\'d encourage you to read (parts of) it, as it does a good job of explaining what the benefits and downsides of checked exceptions are.
回答15:
Anders speaks about the pitfalls of checked exceptions and why he left them out of C# in episode 97 of Software Engineering radio.
回答16:
To attempt to address just the unanswered question:
If you throw RuntimeException subclasses instead of Exception subclasses then how do you know what you are supposed to catch?
The question contains specious reasoning IMHO. Just because the API tells you what it throws doesn\'t mean you deal with it in the same way in all cases.
To put it another way, the exceptions you need to catch vary depending on the context in which you use the component throwing the exception.
For example:
If I\'m writing a connection tester for a database, or something to check the validity of a user entered XPath, then I\'d probably want to catch and report on all checked and unchecked exceptions that are thrown by the operation.
If, however, I am writing a processing engine, I will likely treat an XPathException (checked) in the same way as an NPE: I would let it run up to the top of the worker thread, skip the rest of that batch, log the issue (or send it to a support department for diagnosis) and leave feedback for the user to contact support.
回答17:
The problem
The worst problem I see with exception handling mechanism is that it introduces code duplication in a big scale! Let\'s be honest: In most of projects in 95% of the time all that developers really need to do with exception is to communicate it somehow to the user (and, in some cases, to the development team as well, e.g. by sending an e-mail with the stack trace). So usually the same line/block of code is used in every place the exception is handled.
Let\'s assume that we do simple logging in each catch block for some type of checked exception:
try{
methodDeclaringCheckedException();
}catch(CheckedException e){
logger.error(e);
}
If it\'s a common exception there may be even several hundreds of such try-catch blocks in a larger codebase. Now let\'s assume that we need to introduce popup dialog based exception handling instead of console logging or start to additionally send an e-mail to the development team.
Wait a moment... are we really going to edit all of that several hundreds of locations in the code?! You get my point :-).
The solution
What we did to adress that issue was introducing the concept of exception handlers (to which I\'ll further refer as EH\'s) to centralize exception handling. To every class that needs to hande exceptions an instance of exception handler is injected by our Dependency Injection framework. So the typical pattern of exception handling now looks like this:
try{
methodDeclaringCheckedException();
}catch(CheckedException e){
exceptionHandler.handleError(e);
}
Now to customize our exception handling we only need to change the code in a single place (EH code).
Of course for more complex cases we can implement several subclasses of EHs and leverage features that our DI framework provides us. By changing our DI framework configuration we can easily switch EH implementation globally or provide specific implementations of EH to classes with special exception handling needs (for example using Guice @Named annotation).
That way we can differentiate exception handling behaviour in development and release version of application (eg. development - logging the error and halting the application, prod - logging the error with more details and letting the application continue its execution) with no effort.
Last one thing
Last but not least, it may seem that the same kind of centralisation can be obtained by just passing our exceptions \"up\" until they arrive to some top level exception handling class. But that leads to cluttering of code and signatures of our methods and introduces maintenance problems mentioned by others in this thread.
回答18:
My writeup on c2.com is still mostly unchanged from its original form: CheckedExceptionsAreIncompatibleWithVisitorPattern
In summary:
Visitor Pattern and its relatives are a class of interfaces where the indirect caller and interface implementation both know about an exception but the interface and direct caller form a library that cannot know.
The fundamental assumption of CheckedExceptions is all declared exceptions can be thrown from any point that calls a method with that declaration. The VisitorPattern reveals this assumption to be faulty.
The final result of checked exceptions in cases like these is a lot of otherwise useless code that essentially removes the compiler\'s checked exception constraint at runtime.
As for the underlying problem:
My general idea is the top-level handler needs to interpret the exception and display an appropriate error message. I almost always see either IO exceptions, communication exceptions (for some reason APIs distinguish), or task-fatal errors (program bugs or severe problem on backing server), so this should not be too hard if we allow a stack trace for a severe server problem.
回答19:
This article is the best piece of text on exception handling in Java I have ever read.
It favours unchecked over checked exceptions but this choice is explained very thouroughly and based on strong arguments.
I don\'t want to cite too much of the article content here (it\'s best to read it as a whole) but it covers most of arguments of unchecked exceptions advocates from this thread. Especially this argument (which seems to be quite popular) is covered:
Take the case when the exception was thrown somewhere at the bottom of the API layers and just bubbled up because nobody knew it was even possible for this error to occur, this even though it was a type of error that was very plausible when the calling code threw it (FileNotFoundException for example as opposed to VogonsTrashingEarthExcept... in which case it would not matter if we handle it or not since there is nothing left to handle it with).
The author \"responses\":
It is absolutely incorrect to assume that all runtime exceptions
should not be caught and allowed to propagate to the very \"top\" of the
application. (...) For every exceptional condition that is required to
be handled distinctly - by the system/business requirements -
programmers must decide where to catch it and what to do once the
condition is caught. This must be done strictly according to the
actual needs of the application, not based on a compiler alert. All
other errors must be allowed to freely propagate to the topmost
handler where they would be logged and a graceful (perhaps,
termination) action will be taken.
And the main thought or article is:
When it comes to error handling in software, the only safe and correct assumption that may ever be made is that a failure may occur in absolutely every subroutine or module that exists!
So if \"nobody knew it was even possible for this error to occur\" there is something wrong with that project. Such exception should be handled by at least the most generic exception handler (e.g. the one that handles all Exceptions not handled by more specific handlers) as author suggests.
So sad not many poeple seems to discover this great article :-(. I recommend wholeheartly everyone who hesitates which approach is better to take some time and read it.
回答20:
Checked exceptions were, in their original form, an attempt to handle contingencies rather than failures. The laudable goal was to highlight specific predictable points (unable to connect, file not found, etc) & ensure developers handled these.
What was never included in the original concept, was to force a vast range of systemic & unrecoverable failures to be declared. These failures were never correct to be declared as checked exceptions.
Failures are generally possible in code, and EJB, web & Swing/AWT conta