前言
在程序的运行过程中,各种错误是不可避免的,比如想要读入一个文件,但是文件不存在。
我们可以通过返回特殊值来表示错误(常见于一些较底层的语言如 C),但是它存在一定的局限性,如特殊值的数目有限、有时无法有效区分特殊值和错误……当一个函数返回的就是boolean
类型时,那么就难以规定特殊值;一个函数返回一个Object
类型,那么当返回值为null
时到底是发生了错误还是值就是null
就难以区分。
简介
另一种方法为在语言层面上提供一个异常处理机制。Java
就内置了一套异常处理机制,总是使用异常来表示错误。一个用来处理读取文件时文件不存在的代码。
1 | try { |
这样当文件不存在时程序不会直接终止,而是提示选择其他文件,其中的FileNotFoundException
是一种异常类。
异常类是一种特殊的类,它可以在任何地方抛出,但需要被捕获。如果没有在当前方法捕获,那么这个异常会沿着栈一层层向上传递。如果整个程序中没有捕获这个异常,那么它会传到运行时,最后在控制台显示,并结束整个程序。
继承关系
异常类的继承关系图如下:
1 | ┌───────────┐ |
Throwable
表示这个类可以被throw
,即抛出。
其中Error
类表示极其严重的错误,一般来说程序员无法处理,根据快速显示出BUG
的原则,应该立即终止程序,如:
OutOfMemoryError
: 内存耗尽,无法获取足够的内存StackOverflowError
: 栈溢出
而我们讨论的Exception
则是运行时产生的错误,程序员对于这些异常应当捕获并处理。
分类
运行时错误RuntimeException和其他的异常
这是从异常产生的原因所做的分类。
异常分为两大类:运行时错误RuntimeException
和其他的异常。
其中运行时错误的产生是由于程序员对于代码的处理不当,代码存在错误,如访问无效内存。而其他的异常一般是由程序员无法控制的外部原因导致,应当包含在代码的逻辑中,如IO
错误。
运行时错误是可以避免的。若一个运行时错误被抛出,那么此段代码是存在错误的。程序员可以通过提前检验来避免RuntimeException
。如ArrayIndexOutOfBoundsException
完全可以通过事先检验下标是否合法来避免。
其他的异常则较难避免,因为外部因素时难以控制的。如对于FileNotFoundException
,即使我们提前检测了文件是否存在,在程序运行时文件有可能被删除。
Checked Exception和Unchecked Exception
这是从异常处理机制的角度所做的分类。
如果一个异常必须捕获,即该异常要由程序员来处理,那么它就是Checked Exception
,包含Exception
及其除了RuntimeException
的其他子类。而如果一个异常不是必须捕获,那么它就是Unchecked Exception
,包含Error
类和RuntimeException
类以及它们的子类。
对于Unchecked Exception
,程序员不需要使用try...catch
等机制来处理这些异常。编译器会帮助我们处理这些异常。但是它可能会导致程序运行失败,出现潜在的BUG。它类似于编程语言中的dynamic type checking
,错误发生在运行时。
对于Checked Exception
,我们必须使用try...catch
来处理,或者在方法声明时要添加到throws
中,让调动它的方法来处理。如果我们不使用这些机制显试地处理这些异常,那么程序会报错,类似于编程语言中的static type checking
,在编译期就会检测错误。
当然,对于Unchecked Exception
我们也可以采用try...catch
机制来处理,但大多数时候是不需要的,也不应该这么做。这样做不利于BUG暴露出来。我们要让BUG较早显现,而不是掩耳盗铃。
当要决定是采用Unchecked Exception
还是Checked Exception
的时候,问一个问题:“如果这种异常一旦抛出,客户端会采用怎样的补救?”
如果客户端可以通过其他的方法恢复异常,那么采用Checked Exception
如果客户端对出现的异常无能为力,那么采用Unchecked Exception
一般我们应当努力的恢复,而不是简单地打印错误。