第11章 异常,日志,断言和调试(异常部分)
__11_1 处理异常
在程序出现错误时
- 返回到一种安全状态,并能够让用户执行一些其他的命令;或者
- 允许用户保存所有的操作结果,并以适当的方式终止程序
要做到这些并不容易,因为检测(或引发)错误条件的代码通常离那写能够让数据恢复到安全状态,或者能够保存用户的操作结果,并正常地退出程序的代码很远。异常处理的任务就是将控制权从错误产生的地方转移给能够处理这种情况的错误处理器。
程序中可能出现的错误包括
- 用户输入错误
- 设备错误(打印机被关掉,网页不能浏览,,,)
- 物理限制(磁盘已满,可用存储空间用完,,,)
- 代码错误
1)异常分类
Java中所有异常对象都派生于Throwable类。
异常层次结构如下图:
Error类描述了Java运行时系统的内部错误和资源耗尽错误。应用程序不应该抛出这种类型的对象,如果出现了这样的内部错误,除了通告给用户,并尽力使程序安全终止之外,再无能为力了。
由程序错误导致的异常属于RuntimeException,而程序本身没有问题,但由于I/O错误这类问题导致的异常属于其他异常。
派生于RuntimeException的异常包含
- 错误的类型转换
- 数组访问越界
- 访问空指针
不是派生于RuntimeException的异常包括
- 试图在文件尾部后面读取数据
- 试图打开一个错误格式的URL
- 试图根据给定的字符串查找Class对象,而这个字符串表示的类不存在
“如果出现RuntimeException异常,那么就一定是你的问题”
Java语言将派生于Error类或RuntimeException类的所有异常称为未检查(unchecked)异常,所有其他异常称为已检查(checked)异常。编译器将核查是否为所有已检查异常提供了异常处理器。
2)声明已检查异常
public FileInputStream(String name) throws FileNotFoundException
这个声明表示这个构造器有可能抛出一个FileNotFoundException异常。如果发生了这种糟糕情况,构造器将不会初始化一个新的FileInputStream对象,而是抛出一个FileNotFoundException对象。
在编写方法时不必将所有可能抛出的异常进行声明,在遇到下述四种情况时应该抛出异常:
- 调用一个抛出已检查异常的方法
- 程序运行过程中发现错误,并利用throw语句抛出一个已检查异常
- 程序出现错误,例如,a[-1] = 0会抛出一个ArrayIndexOutOfBoundsException这样的未检查异常
- Java虚拟机和运行时库出现的内部异常
如果出现前两种情况之一,则必须告诉调用这个方法的程序员有可能抛出异常。对于可能被他人使用的Java方法,应该根据异常规范(exception specification),在方法的首部声明这个方法可能抛出的异常。
class MyAnimation
{
...
public Image loadImage(String s) throws IOException, MalformedURLException
{
...
}
}
但是需要声明Java内部的错误,即从Error继承的错误,任何程序代码都具有抛出那些异常的潜能,而我们对其没有任何控制能力。
一个方法必须声明所有可能抛出的已检查异常,而为检查异常中,Error不可控制,RuntimeException应当避免。捕获异常可以使异常不被抛到方法之外,也不需要throws规范。
如果子类中覆盖了超类的一个方法,子类中声明的已检查异常不能超过超类方法中声明的异常范围
如果类中的一个方法声明将会抛出一个异常,而这个异常是某个特定类的实例时,则这个方法就有可能抛出一个这个类的异常,或者这个类任意子类的异常。例如FileInputStream声明抛出了IOException异常,它既可能是IOException异常,也可能是其子类的异常,例如FileNotFoundException。
在Java中,没有throws说明符的方法将不能抛出任何已检查异常。
3)如何抛出异常
首先决定要抛出什么样的异常,如EOFException异常描述的是“在输入过程中,遇到了一个未预期的EOF后的信号”,若这是我们遇到的情况,则可选择抛出EOFException异常。
String readData(Scanner in) throws EOFException
{
...
while(...)
{
if(!in.hasNext())//EOF encountered
{
if(n < len)
{
throw new EOFException();
}
...
}
return s;
}
}
同时,可通过借助异常类的构造方法等方式返回更多关于异常的信息,如:
String gripe = "Content-length: " + len + ", Received: " + n;
throw new EOFException(gripe);
一旦方法抛出了异常,这个方法就不可能返回到调用者。也就是说,不必为返回的默认值或错误代码担忧。
4)创建异常类
定义一个派生于Exception,或Exception子类的类即可。通常定义两个构造器,其一使默认的构造器,另一个则是带有详细描述信息的构造器,这些描述信息可以由Throwable的toSting方法打印出。
__11_2 捕获异常
通常,最好的处理异常的选择就是什么也不做,将异常传递给调用者。如InputStream类的read方法。如果采用这种方式,就必须声明这个方法可能会抛出一个IOException。
如果调用了一个抛出已检查异常的方法,就必须对它进行处理,或者将它传递出去。
由于覆盖超类的方法时,子类中声明的已检查异常不能超过超类方法中声明的异常范围,故若超类中的这个方法没有抛出异常,这个方法就必须捕获方法代码中出现的每一个已检查异常(如JComponent中的paintComponent)。
1)捕获多个异常
try
{
...
}
catch (MalformedURLException e1)
{
System.out.println(e1.getMessage());
}
catch (UnknownHostException e2)
{
System.out.println(e2.getClass().getName());
}
2)再次抛出异常与异常链
catch子句中可以抛出一个异常,这么做的目的是改变异常的类型。如果开发了一个供其他程序员使用的子系统,用于表示子系统故障的异常类型可能有多种解释。ServletException就是这样一个异常的例子,执行执行servlet的代码可能不想知道错误的细节原因,但希望明确知道servlet是否有故障。
try
{
access the database
}
catch(SQLException e)
{
throw new ServeletException("database error: " + e.getMessage();
}
在Java SE 1.4中,可以将原始异常设置为新异常的“诱饵”:
//抛出异常
try
{
access the database
}
catch(SQLException e)
{
Throwable se = new ServeletException("database error");
se.initCause(e);
throw se;
}
捕获到异常时可以使用下面这条语句重新得到原始异常:
Throwable e = se.getCause();
使用这种包装技术可以让用户抛出子系统的高级异常,而不会丢失原始异常的细节。
如果一个方法中发生了一个已检查异常,而不允许抛出它,那么包装技术就十分有用。我们可以捕获这个已检查异常,将它包装成一个运行时异常。
3)Finally子句
当代码抛出异常时会终止方法中剩余代码的处理并退出这个方法的执行。如果此时方法获得了一些本地资源,只有这个方法知道,并需要在退出方法之前回收这些资源,就会产生资源回收问题,可以通过:
- 捕获并重新抛出所有的异常
- 使用finally子句
Graphics g = image.getGraphics();
try
{
...
}
catch(IOEception e)
{
...
}
finally
{
g.dispose();
}
finally子句会在如下三种情况执行:
没有抛出异常
在catch子句抛出异常
执行try至异常发生,执行相应catch至抛出异常,执行finally子句在catch语句中没有抛出异常
执行try至异常发生,执行catch语句,执行finally语句抛出了一个异常,但没有捕获
程序将执行try至有异常被抛出,然后执行finally子句中的语句,并将异常抛给这个方法的调用者
Java核心技术中建议使用下述格式处理异常:
InputStream in = ...;
try
{
try
{
//code that might throw exceptions
}
finally
{
in.close();
}
}
catch(IOException e)
{
//show error dialog
}
- 当final子句包含return语句时,即使已经利用return从try语句块中退出,在方法返回前依然会执行finally子句,如果此时finally子句中也有一个return语句,则这个返回值会覆盖原始的返回值。
- 假设在执行try语句块中的代码抛出了一些非IOException的异常,在执行finally语句块时,finally语句块中的方法也抛出IOException异常,这种情况下,会使原始的异常丢失,转而抛出IOException异常。
4)分析堆栈跟踪元素
堆栈跟踪(stack trace)是一个方法调用过程的列表,它包含了程序执行过程中方法调用的特定位置。
可以使用Throwable类的printStackTrace方法访问堆栈跟踪的文本描述信息。
在Java SE 1.4以后,可以调用getStackTrace方法获得StackTraceElement对象的数组,并在程序中对它进行分析。StackTraceElement类含有能够获得文件名和当前执行代码行号的方法,同时还含有能够获得类名和方法名的方法,toString方法将获得一个格式化字符串。
Throwable t = new Throwable();
StackTraceElement[] frames = t.getStackTrace();
for(StackTraceElement fram : frames)
analyze frame
Java SE 5.0中,增加了静态的Thread.getAllStackTrace方法,可以产生所有线程的堆栈跟踪。
Map<Thread, StackTraceEleent[]> map = Thread.getAllStackTraces();
for(Thread t : map.keySet())
{
StackTraceElement[] frames = map.get(t);
}
__11_3 使用异常机制的建议
- 与执行简单的测试相比,捕获异常所花费的时间大大超过了前者,因此只应在异常情况下使用异常机制;
- 将整个任务包装在一个try语句块中;这样,当任何一个操作出现问题时,整个任务都可以被取消
- 利用异常层次结构;将一种异常转换为另一种更加合适的异常时不要犹豫。例如,在解析某文件中的一个整数时,捕获NumberFormatException异常,然后将它转换成IOException或MySubsystemException的子类
- 不要压制异常;
- 检测错误时,“苛刻”比放任好;当栈空时,Stack.pop使返回null还是抛出一个异常?我们认为:在出错的地方抛出一个EmptyStackException异常要比在后面抛出一个NullPointerException好
- 不要羞于传递异常;传递异常要比捕获这些异常更好,让高层次的方法通告用户发生了错误,或者放弃不成功的命令更加适宜