不及格的程序员-八神

1、对Checked Exceptions特性持保留态度

（译者注：在写一段程序时，如果没有用try-catch捕捉异常或者显式的抛出异常，而希望程序自动抛出，一些语言的编译器不会允许编译通过，如Java就是这样。这就是Checked Exceptions最基本的意思。该特性的目的是保证程序的安全性和健壮性。Zee&Snakey(MVP)对此有一段很形象的话，可以参见：

http：//www.blogcn.com/user2/zee/main.asp。Bruce Eckel 也有相关的一篇文章（《Does Java need Checked Exceptions》），参见：

http：//www.mindview.net/Etc/Discussions/CheckedExceptions）

Bruce Eckel：C#没有Checked Exceptions，你是怎么决定是否在C#中放置这种特性的么？

Anders Hejlsberg：我发现Checked Exceptions在两个方面有比较大的问题：扩展性和版本控制。我知道你也写了一些关于Checked Exceptions的东西，并且倾向于我们对这个问题的看法。

Bruce Eckel：我一直认为Checked Exceptions是非常重要的。

Anders Hejlsberg：是的，老实说，它看起来的确相当重要，这个观点并没有错。我也十分赞许Checked Exceptions特性的美妙。但它某些方面的实现会带来一些问题。例如，从Java中Checked Exceptions的实现途径来看，我认为它在解决一系列既有问题的同时，付出了带来一系列新问题的代价。这样一来，我就搞不清楚Checked Exceptions特性是否可以真的让我们的生活变得更美妙一些。对此你或许有不同看法。

Bruce Eckel：C#设计小组对Checked Exceptions特性是否有过大量的争论？

Anders Hejlsberg：不，在这个问题上，我们有着广泛的共识。C#目前在Checked Exceptions上是保持缄默的。一旦有公认的更好的解决方案，我们会重新考虑，并在适当的地方采用的。我有一个人生信条，那就是——如果你对该问题不具有发言权，也没办法推进其解决进程，那么最好保持沉默和中立，而不应该摆出一个非此即彼的架势。

Bruce Eckel：极限编程(The Extreme Programmers)上说：“用最简单的办法来完成工作。”

Anders Hejlsberg：对呀，爱因斯坦也说过：“尽可能简单行事。”对于Checked Excpetions特性，我最关心的是它可能给程序员带来哪些问题。试想一下，当程序员调用一些新编写的有自己特定的异常抛出句法的API时，程序将变得多么纷乱和冗长。这时候你会明白Checked Exceptions不是在帮助程序员，反而是在添麻烦。正确的做法是，API的设计者告诉你如何去处理异常而不是让你自己想破脑袋。

2、Checked Exceptions的版本相关性

Bill Venners：你提到过Checked Exceptions的扩展性和版本相关性这两个问题。现在能具体解释一下它们的意思么？

Anders Hejlsberg：让我首先谈谈版本相关性，这个问题更容易理解。假设我创建了一个方法foo，并声明它可能抛出A、B、C三个异常。在新版的foo中，我要增加一些功能，由此可能需要抛出异常D。这将产生了一个极具破坏性的改变，因为原来调用此方法时几乎不可能处理过D异常。

??? 也就是说，在新版本中增加抛出的异常时，给用户的代码带来了破坏。在接口中使用方法时也有类似的问题。一个实现特定功能的接口一经发布，就是不可改变的，新功能只能在新版的接口中增加。换句话说，就是只能创建新的接口。在新版本中，你只有两种选择，要么建立一个新的方法foo2，foo2可以抛出更多的异常，要么在新的foo中捕获异常D，并转化为原来的异常A、B或者C。

Bill Venners：但即使在没有Checked Exceptions特性的语言中，（增加新的异常）不是同样会对程序造成破坏么？假如新版foo抛出了需要用户处理的新的异常，难道仅仅因为用户不希望这个异常发生，他写代码时就可以置之不理吗？

Anders Hejlsberg：不，因为在很多情况下，用户根本就不关心（异常）。他们不会处理任何异常。其实消息循环中存在一个最终的异常处理者，它会显示一个对话框提示你程序运行出错。程序员在任何地方都可以使用try finally来保护自己的代码，即使运行时发生了异常，程序依然可以正确运行。对于异常本身的处理，事实上，程序员是不关心的。

Bill Venners：如此说来，你认为不要求程序员明确的处理每个异常的做法，在现实中要适用得多了？

Anders Hejlsberg：人们为什么认为（显式的）异常处理非常重要呢？这太可笑了。它根本就不重要。在我印象中，一个写得非常好的程序里，try finally和try catch语句数目大概是10：1。在C#中，也可以使用和类似try finally的using语句（来处理异常）。

Bill Venners：finally到底干了些什么？

Anders Hejlsberg：finally保证你不被异常干扰，但它不直接处理异常。异常处理应该放在别的什么地方。实际上，在任何一个事件驱动的（如现代图形界面）程序中，在主消息循环里，都有一个缺省的异常处理过程，程序员只需要处理那些没被缺省处理的异常。但你必须确保任何异常情况下，原来分配的资源都能被销毁。这样一来，你的程序就是可持续运行的。你肯定不希望写程序时，在100个地方都要处理异常并弹出对话框吧。如果那样的话，你作修改时就要倒大霉了。异常应该集中处理，并在异常来临处保护好你的代码。

3、Checked Exceptions的扩展性

Bill Venners：那么Checked Exceptions的扩展性又是如何呢？

Anders Hejlsberg：扩展性有时候和版本性是相关的。 在一个小程序里，Checked Exceptions显得蛮迷人的。你可以捕捉FileNotFoundException异常并显示出来，是不是很有趣？这在调用单个的API时也挺美妙的。但是在开发大系统时，灾难就降临了。你计划包含4、5个子系统，每个子系统抛出4到10个异常。但是（实际开发时），你每在系统集成的梯子上爬一级，必须被处理的新异常都将呈指数增长。最后，可能每个子系统需要抛出40个异常。将两个子系统集成时，你将必须写80个throw语句。最后，可能你都无法控制了。

Java异常：选择Checked Exception还是Unchecked Exception?

分类：

Java包含两种异常：checked异常unchecked异常。C#只有unchecked异常。checked和unchecked异常之间的区别是：

1. Checked异常必须被显式地捕获或者传递，如Basic try-catch-finally Exception Handling一文中所说。而unchecked异常则可以不必捕获或抛出。
2. Checked异常继承java.lang.Exception类。Unchecked异常继承自java.lang.RuntimeException类。

Checked和unchecked异常从功能的角度来讲是等价的。可以用checked异常实现的功能必然也可以用unchecked异常实现，反之亦然。

一个简单的例子

[java] view plain

1. public void storeDataFromUrl(String url){
2.     try {
4.     } catch (BadUrlException e) {
5.         e.printStackTrace();
6.     }
7. }
8.
13.     }
14.
15.     String data = null;
16.     //read lots of data over HTTP and return
17.     //it as a String instance.
18.
19.     return data;
20. }

[java] view plain

1. public class BadUrlException extends Exception {
3.         super(s);
4.     }
5. }

[java] view plain

1. public void storeDataFromUrl(String url)
4. }

[java] view plain

1. public class BadUrlException extends RuntimeException {
3.         super(s);
4.     }
5. }

[java] view plain

1. public void storeDataFromUrl(String url){
3. }
4.
5. public String readDataFromUrl(String url) {
8.     }
9.
10.     String data = null;
11.     //read lots of data over HTTP and
12.     //return it as a String instance.
13.
14.     return data;
15. }

Checked 还是Unchecked？

1. 支持Checked异常：
编译器强制检查，checked异常必须被捕获或者传播，这样就不会忘记处理异常。
2. 支持Checked异常：
Unchecked异常容易忘记处理，由于编译器不强制程序员捕获或传播它（第一条的反面表述）。
3. 支持Unchecked异常：
沿调用栈向上传播的Checked异常破坏了顶层的方法，因为这些方法必须声明抛出所有它们调用的方法抛出的异常。
4. 支持Checked异常：
当方法不声明它们会抛出何种异常时，就难以处理它们抛出的异常。
5. 支持Unchecked异常：
Check异常的抛出作为方法接口的一部分，这使得添加或移除早期版本中方法的异常难以实现。

观点1（支持Checked异常）：

相反观点：

[java] view plain

1. try{
2.    callMethodThatThrowsException();
3. catch(Exception e){
4. }

观点2（支持Checked异常）：

Unchecked异常容易忘记处理，由于编译器不强制程序员捕获或传播它（第一条的反面表述）。

观点3（支持Unchecked异常）：

[java] view plain

4.     long number = convertData(data);
5.     return number;
6. }
7.
11.    //read data and return it.
12. }
13.
14. private long convertData(String data)
16.     //convert data to long.
17.     //throw BadNumberException if number isn't within valid range.
18. }

相反观点1：

[java] view plain

2. throws ApplicationException{
3.     try{
5.         long number = convertData(data);
7.         throw new ApplicationException(e);
9.         throw new ApplicationException(e);
10.     }
11. }

[java] view plain

3.     long number = convertData(data);
4. }

[java] view plain

2.     try{
4.         long number = convertData(data);
6.         throw new ApplicationException(
7.             "Error reading number from URL", e);
9.         throw new ApplicationException(
10.             "Error reading number from URL", e);
11.     }
12. }

相反观点2：

[java] view plain

2. throws ApplicationException {
4.     long number = convertData(data);
5.     return number;
6. }
7.
11.    //read data and return it.
12. }
13.
14. private long convertData(String data)
16.     //convert data to long.
17.     //throw BadNumberException if number isn't within valid range.
18. }
19.
20.
21. public class ApplicationException extends Exception{ }
22. public class BadNumberException   extends ApplicationException{}
23. public class BadUrlException      extends ApplicationException{}

观点5（支持Unchecked异常）：

Check异常的抛出作为方法接口的一部分，这使得添加或移除早期版本中方法的异常难以实现。

总结

• Unchecked异常不会使代码显得杂乱，因为其避免了不必要的try-catch块。
• Unchecked异常不会因为异常声明聚集使方法声明显得杂乱。
• 关于容易忘记处理unchecked异常的观点在我的实践中没有发生。
• 关于无法获知如何处理未声明异常的观点在我的实践中没有发生。
• Unchecked异常避免了版本问题。

Anders Hejlsberg on checked vs. unchecked exceptions
http://www.artima.com/intv/handcuffs.html

James Gosling on checked exceptions
http://www.artima.com/intv/solid.html

Bill Venners on Exceptions
http://www.artima.com/interfacedesign/exceptions.html

Bruce Eckel on checked exceptions
http://www.artima.com/intv/typingP.html

Designing with Exceptions (Bill Venners - www.artima.com)
http://www.artima.com/designtechniques/desexcept.html

Effective Java (Joshua Bloch - Addison Wesley 2001)

Daniel Pietraru - in favor of checked exceptions
http://littletutorials.com/2008/05/06/exceptional-java-checked-exceptions-are-priceless-for-everything-else-there-is-the-the-runtimeexception/

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 anadditional 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.*;
private final String stackTrace;
public Exception originalException;
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();
}
} 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)


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.

Kotlin 和 Checked Exception

冷静一下

Google’s Java-centric Android mobile development platform is adding the Kotlin language as an officially supported development language, and will include it in the Android Studio 3.0 IDE.

Android 显然不可能抛弃 Java 而拥抱 Kotlin。毕竟现有的 Android 代码绝大部分都是 Java 写的，绝大部分程序员都在用 Java。很多人都知道 Java 的好处，所以他们不会愿意换用一个新的，未经时间考验的语言。所以虽然 Kotlin 在 Android 上得到了和 Java 平起平坐的地位，想要程序员们从 Java 转到 Kotlin，却不是一件容易的事情。

Checked Exception（CE）的重要性

void foo(string filename) throws FileNotFoundException
{
if (...)
{
throw new FileNotFoundException();
}
...
}


Java 要求你必须在函数头部写上“throws FileNotFoundException”，否则它就不能编译。这个声明表示函数在某些情况下，会抛出 FileNotFoundException 这个异常。由于编译器看到了这个声明，它会严格检查你对 foo 函数的用法。在调用 foo 的时候，你必须使用 try-catch 处理这个异常，或者在调用的函数头部也声明 “throws FileNotFoundException”，把这个异常传递给上一层调用者。

try
{
foo("blah");
}
catch (FileNotFoundException e)
{
...
}


Kotlin 的文档明确的说明，它不支持类似 Java 的 checked exception（CE），指出 CE 的缺点是“繁琐”，并且列举了几个普通程序员心目中“大牛”的文章，想以此来证明为什么 Java 的 CE 是一个错误，为什么它不解决问题，却带来了麻烦。这些人包括了 Bruce Eckel 和 C# 的设计者 Anders Hejlsberg

try
{
foo();
}
catch (Exception)
{
...
}


CE 看起来有点费事，似乎只是为了“让编译器开心”，然而这其实是每个程序员必须理解的事情。出错处理并不是 Java 所特有的东西，就算你用 C 语言，也会遇到本质一样的问题。使用任何语言都无法逃脱这个问题，所以必须把它想清楚。在《编程的智慧》一文中，我已经讲述了如何正确的进行出错处理。如果你滥用 CE，当然会有不好的后果，然而如果你使用得当，就会起到事半功倍，提高代码可靠性的效果。

Java 的 CE 其实对应着一种强大的逻辑概念，一种根本性的语言特性，它叫做“union type”。这个特性只存在于 Typed Racket 等一两个不怎么流行的语言里。Union type 也存在于 PySonar 类型推导和 Yin 语言里面。你可以把 Java 的 CE 看成是对 union type 的一种不完美的，丑陋的实现。虽然实现丑陋，写法麻烦，CE 却仍然有着 union type 的基本功能。如果使用得当，union type 不但会让代码的出错处理无懈可击，还可以完美的解决 null 指针等头痛的问题。通过实际使用 Java 的 CE 和 Typed Racket 的 union type 来构建复杂项目，我很确信 CE 的可行性和它带来的好处。

• “大部分程序员不会处理这些 throws 声明的异常，所以他们就给自己的每个函数都加上 throws Exception。这使得 Java 的 CE 完全失效。”
• “大部分程序员根本不在乎这异常是什么，所以他们在程序的最上层加上 catch (Exception)，捕获所有的异常。”
• “有些人的函数最后抛出 80 多种不同的异常，以至于使用者不知道该怎么办。”……

Hejlsberg 把这些不理解 CE 用法，懒惰，滥用它的人作为依据，以至于得出 CE 是没用的特性，以至于不把它放到 C# 里面。由于某些人会误用 CE，结果就让真正理解它的人也不能用它。最后所有人都退化到最笨的情况，大家都只好写 catch (Exception)。在 Java 里，至少有少数人知道应该怎么做，在 C# 里，所有人都被迫退化成最差的 Java 程序员 ;)

结论？

 Java's checked exceptions were a mistake (and here's what I would like to do about it) 1 April 2003 Java's checked exceptions were an experiment. While Java borrows most of its try/catch exception handling from C++, the notion of "checked" exceptions, which must either be caught or explicitly thrown, are a Java addition. By and large, this experiment has failed. You won't find checked exceptions in Java-influenced languages like Ruby or C#. An idea (the idea?) behind C++ style exception handling is a sound one--it allows one to deal with exceptional conditions at an appropriate, perhaps centralized, point in the call stack, which may be far from where the exceptional condition was encountered. Unrecoverable exceptions are common at very low levels of the code--places where we're interacting with I/O and network devices, for example. But these are the very places least likely to know the appropriate response. Do we simply "skip" that action? Try again? Try a different service? Report the problem to the user? To syslog? Allowing the problem to "propagate up" to some caller is a convenient and relatively clean way of dealing with this problem of needing what is essentially out-of-band communication. At first glance, checked exceptions seem like a good idea too. The major risk with unchecked exceptions is that no one will catch them--problems might bubble clear off the stack. Checked exceptions require that callers either deal with the exception or make it known what they are not dealing with. It forces the caller to consider the exceptional case: If you're opening a file, be prepared to handle FileNotFound. If you're connecting to a server, there may be NoRouteToHost. The problem that's introduced here is the impedance between the intention of try/catch exception handling in general (allow exceptional conditions to be handled far from their source) and the implication of checked exceptions in particular (everyone between the thrower and the handler must be aware of the exception that passes through). Even at a second glance, checked exceptions work fairly well. This approach is adequate for self-contained systems, where the distance between the thrower and catcher is small, or for "bottom tier" subsystems, which act as a source for exceptions, but rarely as a sink or pipe (think of basic networking, file I/O, JDBC, etc.). Checked exceptions are pretty much disastrous for the connecting parts of an application's architecture however. These middle-level APIs don't need or generally want to know about the specific types of failure that might occur at lower levels, and are too general purpose to be able to adequately respond to the exceptional conditions that might occur. Jakarta-Commons Pool provides a good example of this phenomenon. The first release of pool didn't allow checked exceptions:   class ObjectPool { Object borrowObject(); void returnObject(Object obj); } But when the generator of pooled objects may throw a checked exception (like Jakarta-Commons DBCP does), extensions of pool were left with two undesirable options--either quietly swallow the checked exception:   class ConnectionFactory implements PoolableObjectFactory { void makeObject() { try { // ... } catch(SQLException e) { return null; } } } or wrap it with some RuntimeException:   class ConnectionFactory implements PoolableObjectFactory { void makeObject() { try { // ... } catch(SQLException e) { throw new RuntimeException(e.toString()); } } } (JDK 1.4's chained exceptions make this better (throw new RuntimeException(e)), but still not great, for reasons I'll discuss below). Both options undermine the utility of the exception handling mechanism. The current (cvs HEAD) version of Jakarta-Commons Pool allows for arbitrary checked exceptions:   class ObjectPool { Object borrowObject() throws Exception; void returnObject(Object obj) throws Exception; } which feels cleaner in some respects, but similarly undermines the utility of the exception handling mechanism. Clients of ObjectPool instances that don't throw checked exceptions in practice still need to behave as if they do. Clients of ObjectPool instances that do throw checked exceptions lose any compile-time checking that exceptions of a given type are handled in some meaningful way (i.e., we lose all indications that we expect a specific exception--SQLException--and instead must deal with the most generic case). This situation isn't any better than it would be if DriverManager.getConnection() simply threw a RuntimeException, and in some respects, is a bit worse since now I have to litter my code with seemingly extraneous "throws Exception" clauses. The DBCP case is unique, since the client of the ObjectPool is also a SQLException-aware type. (Generally DBCP is a pool masquerading as a Driver or DataSource. Pool plugs-in to DBCP, but it is fully encapsulated by it.) In this scenario, a RuntimeException envelope would be sufficient:   class RuntimeSQLException extends RuntimeException { RuntimeSQLException(SQLException e) { exception = e; }   SQLException getSQLException() { return exception; }   private SQLException exception = e; }   class PoolingDriver { Connection connect(String url, Properties props) throws SQLException { try { return (Connection)(pool.borrowObject()); } catch(RuntimeSQLException e) { throw e.getSQLException(); } }   private ObjectPool pool; }   class ConnectionFactory implements PoolableObjectFactory { void makeObject() { try { // ... } catch(SQLException e) { throw new RuntimeSQLException(e); } } } but this approach only works when there is somebody upstream from ConnectionFactory who knows what to do with a RuntimeSQLException--i.e., when we have control over the code on both sides of the code that the exception passes through. The problem is thornier when the source for the checked exceptions (which creates the "envelope") may be a different component than the one that wants to open the envelope. Jakarta-Commons Functor is a good example of this. The generic functor interfaces don't want to (and hopefully don't need to) be aware of the various checked exceptions that an implementation of that interface might encounter. At the same time, much of the point of an API like Functor is to allow a client of the functor interfaces to interoperate with disparate implementations of those interfaces, so an approach that requires clients to be aware of every functor implementation's RuntimeXXXException subtype (RuntimeIOException, RuntimeSAXException, etc.) isn't desirable either. I'll suggest that what we need is a single uniform mechanism for tunneling checked Exceptions through APIs that only allow RuntimeException. The exception chaining mechanism in JDK 1.4 supports this, but not in a backwards compatible fashion. Jakarta-Commons Lang has a NestedException type that works in earlier JREs, but not in a forward compatible fashion. But exception chaining is really a different concept. Using chained exceptions alone makes it impossible to distinguish the "chain of exceptions" case from the "tunneling Exception through RuntimeException" case. What we really need is a dedicated adapter type:   class ExceptionRuntimeException extends RuntimeException { ExceptionRuntimeException(Exception e) { exception = e; } void rethrowException() throws Exception { throw e; } /* ...etc... */ Exception e; } Such a type could delegate methods like printStackTrace appropriately, could be used for the exclusive purpose of tunneling checked Exceptions, and is equally valid in JDK 1.3 and JDK 1.4. Placing this type in a small, standalone utility component (of Jakarta-Commons for example), would be a rather minor imposition on clients of components that use it (ExceptionRuntimeException would be a run-time dependency if ever instantiated, but clients who choose to could simply treat at a code level like any other RuntimeException.) Here's an example. Jakarta-Commons Functor has a UnaryPredicate type:   interface UnaryPredicate { boolean test(Object obj); } and methods for filtering a Collection according to some UnaryPredicate:   class CollectionAlgorithms { static Collection select(Iterator iter, UnaryPredicate pred, Collection col) { while(iter.hasNext()) { Object obj = iter.next(); if(pred.test(obj)) { col.add(obj); } } return col; }   static Collection select(Iterator iter, UnaryPredicate pred) { return select(iter,pred,new ArrayList()); } } which might be used as:   Collection allFiles = getListOfFiles(); Collection directoriesOnly = CollectionAlgorithms.select(allFiles.iterator(),new IsDirectory()); The implementation of the IsDirectory predicate might have need for checked exceptions of course:   /** * Given a String representing a file URI, determines * whether the given file is a Directory or not. * @throws ExceptionRuntimeException for a URISyntaxException */ class IsDirectory implements UnaryPredicate { boolean test(Object obj) { URI uri; try { uri = new URI((String)obj); } catch(URISyntaxException e) { throw new ExceptionRuntimeException(e); } File file = new File(uri); return file.isDirectory(); } } but using ExceptionRuntimeException allows us to tunnel through the RuntimeException-based functor API without losing information and without adding "throws Exception" to every method in the Functor API. Whenever we're ready, we can unwrap the underlying checked Exception, and can easily distinguish that case from other instances of RuntimeException:   boolean flag; try { flag = isDirectory.test(someObject); } catch(ClassCastException e) { // thrown when someObject is not a String } catch(NullPointerException e) { // thrown when someObject is null } catch(IllegalArgumentException e) { // thrown when someObject isn't a file URI } catch(ExceptionRuntimeException ere) { // thrown only if some Exception was thrown try { ere.rethrowException(); } catch(URISyntaxException e) { // thrown when someObject isn't a valid URI } catch(Exception e) { // other checked exceptions, which we // could throw as checked or unchecked // as needed throw ere; } } I've grown increasingly fond of this approach, and think I'll try to put something together in the Jakarta Commons sandbox for it. By the way, I still think checked exceptions offer some advantage in the cases I enumerated above. Consider Axion's AxionException for example. By design, the use of AxionException is fully encapsulated within the Axion API. Clients to Axion's external interface should never encounter an AxionException, it's only used internally. But similarly, clients to AxionException should generally never encounter an Axion-specific RuntimeException either--Axion's external interface should throw SQLException almost exclusively. Making AxionException a checked exception makes this constraint much easier to enforce (the JDBC-tier of Axion can easily tell which methods may result in an AxionException and which methods may not), even if it means that many of Axion's internal methods are declared to throw AxionException. Having an ExceptionRuntimeException adapter makes it possible to tunnel AxionExceptions through third-party APIs like Commons Functor. Post script: Thinking it unlikely that I was the first person to have this frustration with checked exceptions I consulted with some of my usual sources. One can find a number of related articles and postings, including Bruce "Thinking in Java" Eckel's Does Java need Checked Exceptions? and Checked Exceptions Are Of Dubious Value and Exception Tunneling on Ward's Wiki. A somewhat contrary position can be found in Alan Griffith's Exceptional Java. Among other guidelines, Alan suggests exception "translation" to wrap or chain exceptions to a predictable, component- or API-specific type. This seems similar to exception tunneling, and perhaps sometimes appropriate, but the thought of having to walk arbitrarily deep exception chains to find the relevant exception gives me pause.   To post comments on this story, please use this blog entry.

posted on 2017-05-25 20:48 不及格的程序员-八神 阅读(...) 评论(...) 编辑 收藏