理论上,Java的异常层次结构有一定的道理:
Throwable*
-> Error (OutOfMemoryError, etc.)
-> Exception (IOException, SQLException, etc.)
-> RuntimeException (IndexOutOfBoundsException, NullPointer, etc.)
这些异常的理论背后实际上是有一定道理的。(不幸的是,由于积累的垃圾代码,实际实现留下了一些问题。)
Error
-派生的Throwable
对象是严重的错误,程序不应该能够从中恢复。(换句话说,通常情况下你不会捕捉到这些错误。)出现其中之一表示整个系统存在严重问题。例如,当您耗尽内存时,这表示某处存在严重的问题,因为理论上来说,GC已经拼命地为您释放空间。捕获这种错误是毫无意义的。
Exception
-派生的Throwable
对象是程序在正常操作期间可以合理预期遇到的所有错误;例如网络错误、文件系统错误等等。事实上,除了那些衍生自RuntimeException
的对象外,程序被要求明确处理这些错误—它们被称为"受检异常"。(当然,糟糕的程序员会通过将其替换为占位符来“处理”这些异常,但这是一个程序员的问题,而不是系统问题。)
RuntimeException
-派生的Throwable
对象略有不同。它们是不一定可以预期但程序在发生时可以合理恢复的错误。因此它们没有被检查(程序没有义务处理这些异常),但如果有合理的方法来处理该情况,它们可能会被处理。通常,这些异常代表某种编程错误(而不是前面的类别中在正常操作中发生的预期错误)。
那么这个层次结构是否有意义呢?在某种程度上似乎是有道理的。Error
由系统抛出,表示可能会使您的程序崩溃的重大故障。RuntimeException
在适当使用时,由系统库(或偶尔是您自己的程序)抛出,并且通常意味着某个人在某个地方搞砸了,但没关系,因为您可能能够从中恢复。其他Exception
对象是预期的错误,实际上是您对象的声明接口的一部分。
但是...
最后一个条目就是问题所在。明确的受检异常毫无疑问是下半身疼痛的严重问题。以这种方式使每个方法在整个调用链中都要处理异常—即使只是重新包装并传递它!—代码被杂乱的异常处理模板填塞到了一起,以至于它与返回状态码并在每个方法调用后处理它们的过程几乎没有什么区别。
如果Java是一种更智能的编程语言,那么受检异常将在编译/链接时检查它们是否在整个系统中得到了正确处理。不幸的是,Java的整个架构并不允许进行这种整个程序分析的层面,结果是,在我看来(但许多人也共享这种看法),它实际上融合了异常处理和错误返回两个世界中最糟糕的方面:您会得到大部分显式错误返回的脚手架,但您也会得到类似于COME FROM
的异常行为。