第7章 异常、断言和日志
异常情况,
例如:可能造成程序崩溃的错误输入, Java 使用一种称为异常处理( exception handing) 的错误捕获机制处理。
7.1 处理错误
如果由于出现错误而使得某些操作没有完成, 程序应该:返回到一种安全状态,并能够让用户执行一些其他的命令;或者允许用户保存所有操作的结果,并以妥善的方式终止程序要做到这些并不是一件很容易的事情。其原因是检测(或引发)错误条件的代码通常离那些能够让数据恢复到安全状态, 或者能够保存用户的操作结果, 并正常地退出程序的代码很远。异常处理的任务就是将控制权从错误产生的地方转移给能够处理这种情况的错误处理器。
用户输入错误
1.设备错误
2.物理限制
3.代码错误
在 Java 中, 如果某个方法不能够采用正常的途径完整它的任务,就可以通过另外一个路径退出方法。在这种情况下,方法并不返回任何值, 而是抛出( throw) 一个封装了错误信息的对象。需要注意的是,这个方法将会立刻退出,并不返回任何值。 此外, 调用这个方法的代码也将无法继续执行,取而代之的是, 异常处理机制开始搜索能够处理这种异常状况的异常处理器 (exception handler )。
7.1.1 异常分类
在 Java 程序设计语言中, 异常对象都是派生于 Throwable 类的一个实例。
Error 类层次结构描述了 Java 运行时系统的内部错误和资源耗尽错误。
应用程序不应该抛出这种类型的对象。 如果出现了这样的内部错误, 除了通告给用户,并尽力使程序安全地终止之外, 再也无能为力了。
Exception 层次结构。
1一个分支派生于 RuntimeException ;划分两个分支的规则是: 由程序错误导致的异常属于 RuntimeException ;
2.另一个分支包含其他异常.而程序本身没有问题, 但由于像 I/O 错误这类问题导致的异常属于其他异常:
派生于 RuntimeException 的异常包含下面几种情况:
1错误的类型转换。
2数组访问越界 。
3访问 null 指针。
7.1.2 声明受查异常
在自己编写方法时, 不必将所有可能抛出的异常都进行声明。至于什么时候需要在方法中用 throws 子句声明异常, 什么异常必须使用 throws 子句声明, 需要记住在遇到下面 4 种情况时应该抛出异常:
1 ) 调用一个抛出受査异常的方法, 例如, FilelnputStream 构造器。
2 ) 程序运行过程中发现错误, 并且利用 throw语句抛出一个受查异常(下一节将详细地介绍 throw 语句)。
3 ) 程序出现错误, 例如,a[-l]=0 会抛出一个 ArraylndexOutOffloundsException 这样的非受查异常。
4 ) Java 虚拟机和运行时库出现的内部错误。
如果一个方法有可能抛出多个受查异常类型, 那么就必须在方法的首部列出所有的异常类。
但是, 不需要声明 Java 的内部错误, 即从 Error 继承的错误。任何程序代码都具有抛出那些异常的潜能, 而我们对其没有任何控制能力。
同样,也不应该声明从 RuntimeException 继承的那些非受查异常。
警告:
** 如果在子类中覆盖了超类的一个方法, 子类方法中声明的受查异常不能比超类方法中声明的异常更通用 (也就是说, 子类方法中可以抛出更特定的异常, 或者根本不抛出任何异常)**。特别需要说明的是, 如果超类方法没有抛出任何受查异常, 子类也不能抛出任何受查异常。
7.1.3 如何抛出异常
throw子句
在前面已经看到, 对于一个已经存在的异常类, 将其抛出非常容易 。 在这种情况下:
1 ) 找到一个合适的异常类。
2 ) 创建这个类的一个对象。
3 ) 将对象抛出。
throw new EOFExceptionQ;
或者
EOFException e = new EOFExceptionO;
throw e;
7.1.4 创建异常类
我们需要做的只是定义一个派生于Exception 的类,或者派生于 Exception 子类的类。例如, 定义一个派生于 IOException 的类。
定义的类应该包含两个构造器, 一个是默认的构造器;另一个是带有详细描述信息
的构造器(超类 Throwable 的 toString 方法将会打印出这些详细信息, 这在调试中非常有用)。
class FileFormatException extends IOException
{
public FileFormatExceptionO {}
public FileFormatException(String gripe) {
super(gripe); } }
【API】java.lang.Throwable 1.0 :
Throwable() 构造一个新的 Throwabie 对象, 这个对象没有详细的描述信息。
Throwable(String message ) 构造一个新的 throwabie 对象, 这个对象带有特定的详细描述信息。习惯上,所有派生的异常类都支持一个默认的构造器和一个带有详细描述信息的构造器。
String getMessage( ) 获得 Throwabie 对象的详细描述信息。
7.2 捕获异常
7.2.1 捕获异常
1 ) 程序将跳过 try语句块的其余代码。
2 ) 程序将执行 catch 子句中的处理器代码
public void read(String filename) {
try
{
InputStream in = new Filei叩utStream(filename);
int b;
while ((b = in.read()3 != -1) {
process input
} }
catch (IOException exception) {
exception.printStackTrace(); } }
7.2.2 捕获多个异常
异常对象可能包含与异常本身有关的信息。要想获得对象的更多信息, 可以试着使用
e.getMessage()
得到详细的错误信息(如果有的话,) 或者使用
e.getClass().getName()
得到异常对象的实际类型。
在 Java SE 7中,同一个 catch 子句中可以捕获多个异常类型。
注释: 捕获多个异常时,异常变量隐含为 final 变量。例如,不能在以下子句体中为 e 赋不同的值:catch (FileNotFoundException | UnknownHostException e) { . . . }
注释:捕获多个异常不仅会让你的代码看起来更简单,还会更高效。 生成的字节码只包含一个对应公共catch子句的代码块。
7.2.3 再次抛出异常和异常链
不过,可以有一种更好的处理方法,并且将原始异常设置为新异常的“ 原因”:
try
{
access the database
}
catch (SQLException e){
Throwable se = new ServletException ("database error");
se.initCause(e);
throw se;
}
当捕获到异常时, 就可以使用下面这条语句重新得到原始异常:
Throwable e = se.getCause() ;
强烈建议使用这种包装技术。这样可以让用户抛出子系统中的高级异常,而不会丢失原始异常的细节。
提示: 如果在一个方法中发生了一个受查异常, 而不允许抛出它, 那么包装技术就十分有用。我们可以捕获这个受查异常, 并将它包装成一个运行时异常
有时你可能只想记录一个异常, 再将它重新抛出, 而不做任何改变:
try
{
access the database
}
catch (Exception e)
{
logger.logOevel, message, e);
throw e;
}
在Java SE 7之前,这种方法存在一个问题。假设这个代码在以下方法中:
public void updateRecord() throws SQLException
Java 编译器查看 catch 块中的 throw 语句, 然后查看 e 的类型, 会指出这个方法可以抛出任何 Exception 而不只是 SQLException。现在这个问题已经有所改进。 编译器会跟踪到e 来自 try块。假设这个 try块中仅有的已检査异常是 SQLException 实例, 另外, 假设 e 在catch 块中未改变, 将外围方法声明为 throws SQLException 就是合法的。
7.2.4 finally子句
不管是否有异常被捕获, finally 子句中的代码都被执行。
try 语句可以只有 finally 子句,而没有 catch 子句。
提示: 这里, 强烈建议解搞合 try/catch 和 try/finally 语句块。 这样可以提高代码的清晰度。例如:
InputStrean in = . . .;
try
{
try
{
code that might throw exceptions
}
finally
{
in.close();
}
}
catch (IOException e)
{
show error message
}
内层的 try语句块只有一个职责, 就是确保关闭输入流。 外层的 try 语句块也只有一个职责, 就是确保报告出现的错误。 这种设计方式不仅清楚, 而且还具有一个功能, 就是将会报告 finally 子句中出现的错误。
警告: 当 finally 子句包含 return 语句时, 将会出现一种意想不到的结果„ 假设利用 return语句从 try语句块中退出。在方法返回前, finally 子句的内容将被执行。如果 finally 子句中也有一个 return 语句, 这个返回值将会覆盖原始的返回值。
有时候, finally 子句也会带来麻烦。例如, 清理资源的方法也有可能抛出异常。
这会有问题, 因为第一个异常很可能更有意思。如果你想做适当的处理, 重新抛出原来的异常, 代码会变得极其繁琐:
InputStream in = . . .;
Exception ex = null ;
try{
try{
code that might throw exceptions
}
catch (Exception e){
ex=e;
throw e;
}
}
finally{
try{
in.close();
}
catch (Exception e){
if (ex = null) throw e;
}
}
7.2.5 带资源的try语句
假设资源属于一个实现了 AutoCloseable 接口的类, Java SE 7 为这种代码模式提供了一个很有用的快捷方式。AutoCloseable 接口有一个方法:void close() throws Exception
注释: 另外,还有一个 Closeable 接口。 这是 AutoCloseable 的子接口, 也包含一个 close方法。 不过, 这个方法声明为抛出一个 IOException。
带资源的 try 语句(try-with-resources) :try后接括号,括号内为资源
还可以指定多个资源: 例如:
try (Scanner in = new Scanner (new FileInputStream("/usr/share/dict/words"), "UTF-8");
PrintWriter out = new PrintWriter("out.txt"))
{
while (in.hasNext())
out.println(in.next().toUpperCase());
}
不论这个块如何退出, in 和 out 都会关闭。如果你用常规方式手动编程, 就需要两个嵌套的 try/finally语句。
上一节已经看到, 如果 try 块抛出一个异常, 而且 close 方法也抛出一个异常,这就会带来一个难题。带资源的 try 语句可以很好地处理这种情况。原来的异常会重新抛出,而 close方法抛出的异常会“被抑制”。 这些异常将自动捕获,并由 addSuppressed 方法增加到原来的异常。 如果对这些异常感兴趣, 可以调用 getSuppressed 方法,它会得到从 close 方法抛出并被抑制的异常列表。
你肯定不想采用这种常规方式编程。 只要需要关闭资源, 就要尽可能使用带资源的 try语句。
注释: 带资源的 try语句自身也可以有 catch 子句和一个 finally 子句。 这些子句会在关闭资源之后执行。 不过在实际中, 一个 try 语句中加入这么多内容可能不是一个好主意。
7.2.6 分析堆栈轨迹元素
堆栈轨迹(stack trace ) 是一个方法调用过程的列表, 它包含了程序执行过程中方法调用的特定位置 。
7.3 使用异常机制的技巧
1.异常处理不能代替简单的测试
作为一个示例, 在这里编写了一段代码, 试着上百万次地对一个空栈进行退栈操作。在实施退栈操作之前, 首先要查看栈是否为空。
if (!s.empty()) s.popO;
接下来,强行进行退栈操作。然后, 捕获 EmptyStackException 异常来告知我们不能这样做。
try
{ s.popO; }
catch (EmptyStackException e) {}
在测试的机器上, 调用 isEmpty 的版本运行时间为 646 毫秒。捕获 EmptyStackException 的版
本运行时间为 21 739 毫秒。
可以看出,与执行简单的测试相比, 捕获异常所花费的时间大大超过了前者, 因此使用
异常的基本规则是:只在异常情况下使用异常机制。
2.不要过分地细化异常
很多程序员习惯将每一条语句都分装在一个独立的 try语句块中,需要统一抛出
3.利用异常层次结构
不要只抛出 RuntimeException 异常。应该寻找更加适当的子类或创建自己的异常类。
不要只捕获 Thowable 异常, 否则,会使程序代码更难读、 更难维护。
4.不要压制异常
5.在检测错误时,“ 苛刻” 要比放任更好
当检测到错误的时候, 有些程序员担心抛出异常。在用无效的参数调用一个方法时,返回一个虚拟的数值, 还是抛出一个异常, 哪种处理方式更好? 例如, 当栈空时,Stack.p0p 是返回一个 null, 还是抛出一个异常? 我们认为:在出错的地方抛出一个 EmptyStackException异常要比在后面抛出一个 NullPointerException 异常更好。
6.不要羞于传递异常
很多程序员都感觉应该捕获抛出的全部异常。如果调用了一个抛出异常的方法,例如,
FilelnputStream 构造器或 readLine 方法,这些方法就会本能地捕获这些可能的异常。其 实, 传递异常要比捕获这些异常更好:
public void readStuff(String filename)
throws IOException // not a sign of shame! {
InputStreaa in = new FilelnputStream(filename);
}
让高层次的方法通知用户发生了错误, 或者放弃不成功的命令更加适宜。
注释: 规则 5、6 可以归纳为“ 早抛出,晚捕获 ”
7.4 使用断言
7.4.1 断言的概念
断言机制允许在测试期间向代码中插入一些检査语句。当代码发布时,这些插人的检测语句将会被自动地移走。
Java 语言引人了关键字 assert。这个关键字有两种形式:assert 条件; 和 assert 条件:表达式;
这两种形式都会对条件进行检测, 如果结果为 false, 则抛出一个 AssertionError 异常。在第二种形式中,表达式将被传人 AssertionError 的构造器, 并转换成一个消息字符串。
注释:“ 表达式” 部分的唯一目的是产生一个消息字符串。AssertionError 对象并不存储表达式的值, 因此, 不可能在以后得到它。正如 JDK 文档所描述的那样: 如果使用表达式的值, 就会鼓励程序员试图从断言中恢复程序的运行, 这不符合断言机制的初衷。
7.4.2 启用和禁用断言
在默认情况下, 断言被禁用。可以在运行程序时用 -enableassertions 或 -ea 选项启用 。
需要注意的是, 在启用或禁用断言时不必重新编译程序。启用或禁用断言是类加载器( class loader) 的功能。当断言被禁用时, 类加载器将跳过断言代码, 因此,不会降低程序运行的速度。
也可以在某个类或整个包中使用断言, 例如:
java -ea:MyClass -ea:com.mycompany.mylib... MyApp
这条命令将开启 MyClass 类以及在 com.mycompany.mylib 包和它的子包中的所有类的断言。选项 -ea 将开启默认包中的所有类的断言。
也可以用选项 -disableassertions 或 -da 禁用某个特定类和包的断言:
java -ea:... -da:MyClass MyApp
有些类不是由类加载器加载, 而是直接由虚拟机加载。可以使用这些开关有选择地启用或禁用那些类中的断言。
然而, 启用和禁用所有断言的 -ea 和 -da 开关不能应用到那些没有类加载器的“ 系统类”上。对于这些系统类来说, 需要使用 -enablesystemassertions/-esa 开关启用断言。
在程序中也可以控制类加载器的断言状态。有关这方面的内容请参看本节末尾的 API 注释。
7.4.3 使用断言完成参数检查
在 Java 语言中, 给出了 3 种处理系统错误的机制:
抛出一个异常
日志
使用断言
什么时候应该选择使用断言呢? 请记住下面几点:
断言失败是致命的、 不可恢复的错误。
断言检查只用于开发和测阶段(这种做法有时候被戏称为“ 在靠近海岸时穿上救生衣,但在海中央时就把救生衣抛掉吧”)。
因此,不应该使用断言向程序的其他部分通告发生了可恢复性的错误,或者,不应该作为程序向用户通告问题的手段。断言只应该用于在测试阶段确定程序内部的错误位置。
然而,假设对这个方法的约定做一点微小的改动:
@param a the array to be sorted (must not be null).
现在,这个方法的调用者就必须注意:不允许用 null 数组调用这个方法,并在这个方法
的开头使用断言:
assert a != null;
计算机科学家将这种约定称为前置条件 ( Precondition)。 如果调用者在调用这个方法时没有提供满足这个前置条件的参数, 所有的断言都会失败,并且这个方法可以执行它想做的任何操作。
7.4.4 为文档假设使用断言
前面已经知道, 断言是一种测试和调试阶段所使用的战术性工具; 而日志记录是一种在程序的整个生命周期都可以使用的策略性工具。下一节将介绍日志的相关知识 。