现在我将辩论的问题,是到底还要不要使用“检查型异常”。
我们如果使用“检查型异常”,那么我们在调用有异常方法的时候,就不得不将这方法放入到try-catch块中去,而不论我们是否真的就要在这儿把这个异常给解决掉。一个可行的办法就是,我们如果不需要处理,那么我们可以捕捉后在catch块中使用e.fillInStackTrack()方法将它往上抛,而且还可以保持异常产生到捕捉的轨迹不丢失。不过就算是这样,很明显还是很麻烦,是一个效率低下的办法。我们每天得处理很多这样的异常,真是太麻烦了,所以,很多人反对使用“检查型异常”。
使用“检查型异常”时很容易犯的一个错误就是丢失异常。当我们把一个有有异常声明的方法放到try块中去后,不管我们是否有catch捕捉到它,或是捕捉到后是否有处理,异常处理机制都会默认这个异常已经得到正确的处理了。如:
try {
} finally{
}
try{
} catch(Exception ex) {
//这儿并不做任何事情,异常就这样在这儿消失了,但是根本没有得到有效的处理。
}
有人提出对“检查型异常”进行下面类似的包装:
class WrapCheckedException {
void throwRuntimeException() {
try {
throw new AMyselfException("Where am I?");
} catch(Exception e) { // Adapt to unchecked:
throw new RuntimeException(e);
}
}
}
这样我们调用这个方法的时候,不用把它放入try中去,异常也会被捕捉。而当我们需要在catch块中捕捉它并进行处理的时候,能够使用getCause()方法,如:
catch(RuntimeException re) {
try {
throw re.getCause();
} catch(AMyselfException e) {
e.printStackTrace();
}
不过我觉得这样的包装实在是非常的滑稽。因为这儿在方法中抛出的异常是一个运行期异常,我们以后调用这个方法的时候,根本不知道这个方法会抛出一个怎样的异常,那么我们又怎么会catch(AMyselfException e)呢?
而“检查型异常”附加的那些异常说明(throws)虽然使程序多了一些代码,但当我们不仅仅是要报告发生了一个异常,而且我们还要根据发生了某某异常,而对异常进行处理的时候,这个特别的说明就对我们非常有用了。如果是“非检查型异常”的话,我们根本就不知道我们调用的方法会发生一个具体什么类型的异常,我们最多就是知道这个方法会抛出一个异常。这样的话,我们又如何对异常进行正确的处理?
我们看到,对异常的附加说明是不能够取消的。如果我们一旦取消了这个对异常的说明,那么我们每次想对异常进行处理的时候,不得不再去方法中找到那个抛出异常的地点,看到底是抛出了一个什么具体类型的异常。如果这个方法是第三方库中的方法,而又没有对这个方法将抛出的异常的说明,那么你不得不去询问第三方库生产商。这时候,第三方有可能也需要去检查源代码,才知道那个方法到底抛出了什么类型的异常。可以看到,这样将会造成许多非常麻烦的后果。而如果我们在方法名尾部紧跟了一个对方法将会抛出的异常的说明,那么那么将很轻松的知道我们要处理的方法会抛出什么类型的异常。问题是现在的异常处理机制,每次调用有异常说明的方法的时候,都必须把这个方法放入try块中,这样实在是非常的烦恼。
从上面我总结出来一个综合的异常处理方案:由于考虑到java的向后兼容性,我们现有的异常机制不能进行彻底的动摇。那么,我们可以通过在方法后新增加一个子句,用来说明那些方法中将要抛出的“非检查型异常”。这样的话,调用一个只有“非检”说明子句的方法,我们是不需要将之放入try块中去的。而我们想要更具体的处理异常的时候,我们也能将这方法放入try块中,然后进行捕捉。具体需要捕捉哪些异常呢?在方法的“非检”说明中都有,这样不是很方便吗?
如果以后的异常处理机制进行了上面的改造,那么我们又有一个问题需要提出来。这个问题就是,什么时候我们需要在方法中抛出“检查型异常”?现在在网上也有很多争论,在我看到的中间,有很多都认为“检查型异常”可以不用,是一个java中的一大失败。但我不这么认为,可是我需要一个使用“检查型异常”的理由,给“检查型异常”一个存在的空间。
下面有一个经常发生在我们代码中,但是却十分有趣的现象:
void foo() {
InputStream in = null;
OutputStream out = null;
try {
in = new FileInputStream("foo1.txt");
out = new FileOutputStream("foo2.txt");
// do some stuff with in/out
} catch (IOException e) {
println("something bad happened");
} finally {
if (in != null) {
try {
in.close();
} catch IOException(e) {
println("whooops!");//very crazy!!!
}
}
if (out != null) {
try {
out.close();
} catch (IOException e) {
println("whooops 2!");
}
}
}
}
在上面的代码中我们看到,如果文件没有成功关闭,那么我们还需要写一个try块来处理它,但是这个问题是不可能得到处理的。在这个问题中,我们需要的只是文件没有被关闭这个报告就行了,而且只能做到这样。那么我们完全可以用一个“运行期异常”来代替原来的这个“checked Exception”。这个例子也让我们看到一些人说“运行期异常”在JAVA中完全没有用武之地是错误的观点。
使用“检查型异常”中有一个“bubbling up”的问题。例如:我有一个f()方法,throws exA,exB,exC,exD异常。常常我们偷懒写成throws Exception,这样做非常不好。
使用“Checked Exception”还带来很多的抱怨。例如:我们在方法f()中throws Exception,但是在方法内部,我们并没有throw异常,甚至也没有必要throw异常,也许我们根本不需要,也许我们是为了以后可能发生做好防备。但是,我们在调用f()方法的时候,不管我们愿意还是不愿意,编译器都要求我们把这个方法放入try块中,真是烦恼至极,而且听说在sun的类库中还经常有这样的问题存在。
下面这些内容摘自thinking in java第三版:
Exception guidelines
- Handle problems at the appropriate level. (Avoid catching exceptions unless you know what to do with them).
在合适的地方处理异常(避免在不知道该如何处理的情竞下去捕捉异常)。
- Fix the problem and call the method that caused the exception again.
解决掉问题,然后重新调用引发问题的方法。
- Patch things up and continue without retrying the method.
修正一下问题,然后绕过那个方法再继续。
- Calculate some alternative result instead of what the method was supposed to produce.
用一些别的计算结果,而不是那个方法将返回的结果。
- Do whatever you can in the current context and rethrow the same exception to a higher context.
把当前你能做完的事情做完,然后把相同的异常抛到更高层。
- Do whatever you can in the current context and throw a different exception to a higher context.
把当前你能做完的事情做完,然后抛一个不同的异常到更高层。
- Terminate the program.
中止程序。
- Simplify. (If your exception scheme makes things more complicated, then it is painful and annoying to use.)
简化(如果你的异常把事情搞得更复杂,那用起来将非常的烦恼和痛苦)。
- Make your library and program safer. (This is a short-term investment for debugging, and a long-term investment for application robustness.)
使你的类库和程序更安装(这既是为调试做短期投资,也是为程序的健壮性做长期投资)。