第十一章 异常、断言
异常分类:
所有的异常都是由Throwable继承而来,但在下一层立即分解为两个分支:Error和Exception。
Error类层次结构描述了Java运行时系统内部错误和资源耗尽错误。这种情况很少出现。
在设计Java程序时,需要关注Exception层次结构。这个层次结构又分解为两个分支:一个分支派生于RuntimeException;另一个分支包含其他异常。划分两个分支的规则是:由程序错误导致的异常属于RuntimeException;而程序本身没有问题,但由于像I/O错误这类问题导致的异常属于其他异常。
派生于RuntimeException的异常包含下面几种情况:
l 错误的类型转换
l 数组访问越界
l 访问空指针
不是派生于RuntimeException的异常包括:
l 试图在文件尾部后面读取数据
l 试图打开一个不存在的文件
l 试图根据给定的字符串产找Class对象
Java语言规范将派生于Error和RuntimeException类的所有异常称为未检查异常,所有其他的异常称为已检查异常。编译器将核查是否为所有的已检查异常提供了异常处理器。
注:RuntimeException这个名字很容易让人混淆。实际上,现在讨论的所有错误都发生在运行时。
声明已检查异常
方法应该在其首部声明所有可能抛出的异常。这样可以从首部反映出这个方法可能抛出哪类已检查异常。例下面是标准类库的一个构造器声明:
public FileInputStream(String name) throws FileNotFoundException
如果发生异常,构造器将不会初始化一个新的FileInputStream对象,而是抛出一个异常对象。
下面四种情况应抛出异常:
l 调用一个抛出已检查异常的方法
l 程序运行过程中发现错误,并且利用throw语句抛出一个已检查异常
l 程序出现错误
l Java虚拟机和运行时出现内部错误
上面是程序可能会抛出异常的四种情况,不必全部用throw声明。例如ArrayIndexOutOfBoundsException这样的异常应该尽量在编程时避免,而不是简单地抛出异常。总之,不应该声明从RuntimeException继承的异常。
对于那些可能被其他人使用的Java方法,应根据异常规范,在方法的首部声明这个方法可能抛出的异常。
Class MyAnimation {
...
public Image loadImage(String s) throws IOException {
...
}
}
总之,一个方法必须声明所有可能抛出的已检查异常,而未检查异常要么不可控制(Error),要么就应该避免发生(RuntimeException)。
当然,除了声明异常外,还可以捕获异常。这样会使异常不被抛出到方法外,也不需要throws规范。
注意:如果在子类中覆盖了超类的一个方法,子类方法中声明的已检查异常不能比超类中的方法中声明的异常更通用(也就是说子类抛出更特定的异常,或者根本不抛出异常)。特别需要说明的是,如果超类方法没有抛出任何已检查异常,子类也不能抛出任何已检查异常。
如何抛出异常
抛出异常的语句:
throw new EOFException() ;
或者
EOFException e = new EOFException() ;
throw e ;
例:
String readData(Scanner in) throws EOFException {
...
while(...){
if(!in.hasNext()) {
throws new EOFException() ;
}
}
}
EOFException类还有一个含有一个字符串型参数的构造器。
String gripe = “Conter-length:” + len + “,Recieved:” + n ;
throw new EOFException(gripe) ;
在前面已经看到,对于一个已经存在的异常类,将其抛出非常容易。在这种情况下:
1) 找到一个合适的异常类
2) 创建这个类的对象
3) 将对象抛出
一旦方法抛出了异常 ,这个方法就不可能返回到调用者。
创建异常类
自定义异常类继承于Exception或其子类。
class FileFormatException extends IOException {
public FileFormatException(String gripe) {
super(gripe) ;
}
}
现在可以抛出自定义异常了
String readData(BufferedReader in) throws FileFormatException {
...
while(...) {
if(ch == -1) {
if(n < length)
throw new FileFormatException() ;
}
}
}
捕获异常
要想捕获异常,必须使用try/catch语句块。
通常,最好的选择是什么也不做,而是将异常传递给调用者。如果采用这种方式要声明这个方法可能抛出的异常。
捕获多个异常
可以为每个异常类型使用单独的一个catch字句
Java SE 7中,同一个catch字句中可以捕获多个异常类型。
catch(FileNotFoundException | UnknownHostException e)
只有当捕获的异常类型之间不存在子类关系时才需要这个特性。
再次抛出异常与异常链
在catch字句中可以抛出一个异常,这样做的目的是改变异常的类型。
finally字句
当代码抛出一个异常时,就会终止方法中剩余代码的处理,并退出这个方法的执行。
不管是否有异常被捕获,finally字句中的代码都被执行。
InputStream in = new FileInputStream(...) ;
try
{
//1
code that might throw exceptions
//2
}
catch(IOException e)
{
//3
show error message
//4
}
finally
{
//5
in.close() ;
}
//6
在上面这段代码中,有下列三种情况会执行finally子句:
1.代码中没有抛出异常。执行1、2、5、6
2.抛出一个在catch子句中的捕获的异常。首先执行try语句块中的代码,遇到异常,执行catch中的代码,最后执行finally中的代码。如果catch子句没有抛出异常,程序将执行try语句块之后的第一条语句。在这里执行标注1、3、4、5、6处的语句。如果catch子句没有抛出异常,异常将被抛回这个方法的调用者。在这里。执行标注1、3、5处的语句。
3.代码抛出一个异常,但这个异常不是由catch子句捕获的。在这种情况下,程序将执行try语句块中的所有语句,直到有异常被抛出为止。此时,将跳过try语句块中的剩余代码,然后执行finally子句中的语句,并将异常抛给这个方法的调用者。在这里,执行标注1、5处的子句。
try语句可以只有finally子句,而没有catch子句。例:
InputStream in = ... ;
try
{
code that might throw exceptions
}
finally
{
in.close() ;
}
如果真有异常,这个异常将会被重新抛出,并且必须由另一个catch子句捕获。
InputStream in = ... ;
try
{
try
{
code that might throw exceptions
}
finally
{
in.close() ;
}
}
catch(IOException e)
{
show error message
}
finally子句依然可能抛出异常,需要作出处理,比较繁琐。而且finally中的异常会掩盖try语句中的异常。Java SE 7提供了一种比较方便的方法,那就是带资源的try语句。
带资源的try语句
open a resource
{
work with the resourse
}
finally
{
close the resourse
}
假设资源属于一个实现了AutoCloseable接口的类,对于以上模式,Java SE 7提供了一种快捷方式。
try(Resourse res = ...)
{
work with res
}
try块退出时,会自动调用res.close()
try(Scanner in = new Scanner(new FileInputStream(‘/usr/share/dict/works’)))
{
while(in.hasNext())
System.out.println(in.next())
}
这个块正常退出时,或者存在一个异常时,都会调用in.close()方法,就好像使用了finally块一样。
如果try块中和资源关闭时都发生异常,close方法的异常被抑制,这些异常自动捕获,并由addSuppressed方法增加到原来的异常。可以调用getSuppressed方法,它会得到从close方法抛出并被抑制的异常。
分析堆栈跟踪元素
堆栈跟踪(stack trace)是一个方法调用过程的列表,它包含了程序执行过程中方法调用的特定位置。
可以调用Throwable类的prinStackTrace方法访问堆栈跟踪的文本描述信息。
Throwable t = new Throwable() ;
ByteArrayOutputStream out = new ByteArrayOutputStream() ;
t.printStackTrace(out) ;
String description = out.toString() ;
一种更灵活的方法是使用getStackTrace方法,它会得到StackTraceElement对象的一个数组,可以在你的程序中分析这个数组。例如:
Throwable t = new Throwable() ;
StackTraceElement[] frames = t.getStackTrace() ;
for(StackTraceElement frame : frames)
analyze frame
静态的Thread.getAllStackTrace方法,它可以产生所有线程的堆栈跟踪。下面给出使用这个方法的具体方式:
Map<Thread, StackTraceElement[]> map = Thread.getAllStackTraces() ;
for(Thread t : map.keySet)
{
StackTraceElement[] frames = map.get(t) ;
analyze frames
}
使用异常机制的技巧
1) 异常不能代替简单地测试
2) 不要过分的细化异常
3) 利用异常层次
4) 不要压制异常
5) 在检测错误时,苛刻比放任更好
6) 不要羞于传递异常