20220424 Java核心技术 卷1 基础知识 7
异常、断言和曰志
处 理 错 误
在 Java 中, 如果某个方法不能够采用正常的途径完整它的任务,就可以通过另外一个路径退出方法。在这种情况下,方法并不返回任何值, 而是抛出 ( throw ) 一个封装了错误信息的对象。需要注意的是,这个方法将会立刻退出,并不返回任何值。 此外, 调用这个方法的代码也将无法继续执行,取而代之的是, 异常处理机制开始搜索能够处理这种异常状况的 异常处理器 (exception handler ) 。
异常分类
在 Java 程序设计语言中, 异常对象都是派生于 Throwable
类的一个实例。
所有的异常都是由 Throwable
继承而来,但在下一层立即分解为两个分支: Error
和 Exception
Error
类层次结构描述了 Java 运行时系统的内部错误和资源耗尽错误。 应用程序不应该抛出这种类型的对象。 如果出现了这样的内部错误, 除了通告给用户,并尽力使程序安全地终止之外, 再也无能为力了。这种情况很少出现。
在设计 Java 程序时, 需要关注 Exception
层次结构。 这个层次结构又分解为两个分支:一个分支派生于 RuntimeException
; 另一个分支包含其他异常。划分两个分支的规则是: 由程序错误导致的异常属于 RuntimeException
; 而程序本身没有问题, 但由于像 I/O 错误这类问题导致的异常属于其他异常。
派生于 RuntimeException
的异常包含下面几种情况:
- 错误的类型转换
- 数组访问越界
- 访问
null
指针
不是派生于 RuntimeException
的异常包括:
- 试图在文件尾部后面读取数据
- 试图打开一个不存在的文件
- 试图根据给定的字符串查找
Class
对象, 而这个字符串表示的类并不存在
“ 如果出现 RuntimeException
异常, 那么就一定是你的问题 ” 是一条相当有道理的规则。
Java 语 言 规 范 将 派 生 于 Error
类 或 RuntimeException
类 的所有异常称为 非受查 ( unchecked ) 异常 , 所有其他的异常称为 受查(checked ) 异常 。 编译器将核查是否为所有的受査异常提供了异常处理器。
RuntimeException
这个名字很容易让人混淆。 实际上, 现在讨论的所有错误都发生在运行时。
声明受查异常
方法应该在其首部声明所有可能抛出的异常。这样可以从首部反映出这个方法可能抛出哪类受査异常。
在自己编写方法时, 不必将所有可能抛出的异常都进行声明。至于什么时候需要在方法中用 throws
子句声明异常, 什么异常必须使用 throws
子句声明, 需要记住在遇到下面 4 种情况时应该抛出异常:
- 调用一个抛出受査异常的方法, 例如,
FilelnputStream
构造器 - 程序运行过程中发现错误, 并且利用
throw
语句抛出一个受查异常 - 程序出现错误, 例如,
a[-l]=0
会抛出一个ArrayIndexOutOfBoundsException
这样的非受查异常 - Java 虚拟机和运行时库出现的内部错误
如果一个方法有可能抛出多个受查异常类型, 那么就必须在方法的首部列出所有的异常类。每个异常类之间用逗号隔开。 如下所示:
public Image loadlmage(String s) throws FileNotFoundException, EOFException
{
}
不需要声明 Java 的内部错误, 即从 Error
继承的错误。 也不应该声明从 RuntimeException
继承的那些非受查异常。
如果在子类中覆盖了超类的一个方法, 子类方法中声明的受查异常不能比超类方法中声明的异常更通用 (也就是说, 子类方法中可以抛出更特定的异常, 或者根本不抛出任何异常)。
如何抛出异常
EOFException
异常描述的是 “ 在输入过程中, 遇到了一个未预期的 EOF 后的信号 ”。
对于一个已经存在的异常类, 将其抛出非常容易。在这种情况下:
- 找到一个合适的异常类
- 创建这个类的一个对象
- 将对象抛出
创建异常类
习惯上, 定义的类应该包含两个构造器, 一个是默认的构造器;另一个是带有详细描述信息的构造器(超类 Throwable
的 toString
方法将会打印出这些详细信息, 这在调试中非常有用)。
public class FileFormatException extends IOException {
public FileFormatException() {
}
public FileFormatException(String gripe) {
super(gripe);
}
}
捕获异常
捕获异常
要想捕获一个异常, 必须设置 try/catch 语句块。
如果在 try语句块中的任何代码抛出了一个在 catch 子句中说明的异常类, 那么
- 程序将跳过 try 语句块的其余代码
- 程序将执行 catch 子句中的处理器代码
请记住, 编译器严格地执行 throws
说明符。 如果调用了一个抛出受查异常的方法,就必须对它进行处理, 或者继续传递。
哪种方法更好呢? 通常, 应该捕获那些知道如何处理的异常, 而将那些不知道怎样处理的异常继续进行传递。
如果想传递一个异常, 就必须在方法的首部添加一个 throws
说明符, 以便告知调用者这个方法可能会抛出异常。
这个规则也有一个例外。前面曾经提到过:如果编写一个覆盖超类的方法,而这个方法又没有抛出异常(例如 Runnable
的 run
方法),那么这个方法就必须捕获方法代码中出现的每一个受查异常。不允许在子类的 throws
说明符中出现超过超类方法所列出的异常类范围。
捕获多个异常
在 Java SE 7 中,同一个 catch 子句中可以捕获多个异常类型。
try
{
code that might throw exceptions
}
catch (FileNotFoundException | UnknownHostException e)
{
emergency action for missing files and unknown hosts
}
catch (IOException e)
{
emergency action for all other I/O problems
}
只有当捕获的异常类型彼此之间不存在子类关系时才需要这个特性。
捕获多个异常时,异常变量隐含为 final
变量,不能在以下子句体中为 e 赋不同的值
再次抛出异常与异常链
将原始异常设置为新异常的 “原因”:
try {
access the database
} catch (SQLException e) {
Throwable se = new ServletException("database error");
se.initCause(e);
throw se;
}
当捕获到异常时, 就可以使用下面这条语句重新得到原始异常:
Throwable e = se.getCause();
强烈建议使用这种包装技术。这样可以让用户抛出子系统中的高级异常,而不会丢失原始异常的细节。
如果在一个方法中发生了一个受查异常, 而不允许抛出它, 那么包装技术就十分有用。我们可以捕获这个受查异常, 并将它包装成一个运行时异常。
有时你可能只想记录一个异常, 再将它重新抛出, 而不做任何改变
Java 编译器查看 catch 块中的 throw
语句, 然后查看 e 的类型,发现抛出的是 Exception
,编译器会跟踪到 e 来自 try 块。如果 try 块中的所有受查异常都在方法声明上,那么就是合法的。
private static void test1() throws SQLException, IOException {
try {
int i = 0;
if (i == 0) {
throw new SQLException();
} else {
throw new IOException();
}
} catch (Exception e) {
throw e;
}
}
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 语句, 这个返回值将会覆盖原始的返回值。
带资源的 try 语句
假设资源属于一个实现了 AutoCloseable
接口的类,Java SE 7 为这种代码模式提供了一个很有用的快捷方式。AutoCloseable
接口有一个方法:
void close() throws Exception;
另外,还有一个 Closeable
接口。 这是 AutoCloseable
的子接口, 也包令 1 个 close
方法。 不过, 这个方法声明为抛出一个 IOException
。
带资源的 try 语句(try-with-resources) 的最简形式为:
try (Resource res = . . .) {
work with res
}
try 块退出时,会自动调用 res.close()
。
try-with-resources 还可以指定多个资源:
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());
}
}
带资源的 try 语句自身也可以有 catch 子句和一个 finally 子句。这些子句会在关闭资源之后执行。 不过在实际中, 一个 try 语句中加入这么多内容可能不是一个好主意。
分析堆栈轨迹元素
堆栈轨迹( stack trace ) 是一个方法调用过程的列表, 它包含了程序执行过程中方法调用的特定位置。
可以调用 Throwable
类的 printStackTrace
方法访问堆栈轨迹的文本描述信息
使用 getStackTrace
方法, 它会得到 StackTraceElement
对象的一个数组,StackTraceElement
类含有能够获得文件名和当前执行的代码行号的方法, 同时, 还含有能够获得类名和方法名的方法。
静态的 Thread.getAllStackTrace
方法, 它可以产生所有线程的堆栈轨迹 .
使用异常机制的技巧
- 异常处理不能代替简单的测试
- 不要过分地细化异常
- 利用异常层次结构
- 不要压制异常
- 在检测错误时,“ 苛刻 ” 要比放任更好
- 不要羞于传递异常
规则 5、6 可以归纳为 早抛出,晚捕获
使用断言
断言的概念
假设确信某个属性符合要求, 并且代码的执行依赖于这个属性。例如, 需要计算
double y = Math.sqrt(x);
我们确定 x 不可能是负值的前提下,还是可以添加检查,并抛出异常
if (x < 0) throw new IllegalArgumentException("x < 0");
但是这段代码会一直保留在程序中, 即使测试完毕也不会自动地删除。如果在程序中含有大量的这种检查,程序运行起来会相当慢。
断言机制允许在测试期间向代码中插入一些检査语句。当代码发布时,这些插入的检测语句将会被自动地移走。
Java 语言引人了关键字 assert
。这个关键字有两种形式:
assert 条件;
assert 条件:表达式;
这两种形式都会对条件进行检测, 如果结果为 false
,则抛出一个 AssertionError
异常。在第二种形式中,表达式将被传入 AssertionError
的构造器, 并转换成一个消息字符串。
表达式 部分的唯一目的是产生一个消息字符串。AssertionError
对象并不存储表达式的值, 因此, 不可能在以后得到它。
要想断言 x 是一个非负数值, 只需要简单地使用下面这条语句
assert x >= 0;
或者将 x 的实际值传递给 AssertionError
对象, 从而可以在后面显示出来。
assert x >= 0 : x;
启用和禁用断言
在默认情况下, 断言被禁用。可以在运行程序时用 -enableassertions
或 -ea
选项启用:
java -enableassertions MyApp
在启用或禁用断言时不必重新编译程序。启用或禁用断言是类加载器 ( class loader ) 的功能。当断言被禁用时, 类加载器将跳过断言代码, 因此,不会降低程序运行的速度
也可以在某个类或整个包中使用断言, 例如:
java -ea:MyClass -ea:com.mycompany.mylib... MyApp
这条命令将开启 MyClass
类以及在 com.mycompany.mylib
包和它的子包中的所有类的断言。选项 -ea
将开启默认包中的所有类的断言。
也可以用选项 -disableassertions
或 -da
禁用某个特定类和包的断言
有些类不是由类加载器加载, 而是直接由虚拟机加载。可以使用这些开关有选择地启用或禁用那些类中的断言。
然而, 启用和禁用所有断言的 -ea
和 -da
开关不能应用到那些没有类加载器的 “ 系统类 ” 上。对于这些系统类来说, 需要使用 -enablesystemassertions
/ -esa
开关启用断言。
增加 VM 参数
启用单个类的断言:
-ea:v1ch07.AssertTest
启用包的断言
-ea:v1ch07...
使用断言完成参数检查
在 Java 语言中, 给出了 3 种处理系统错误的机制:
- 抛出一个异常
- 日志
- 使用断言
什么时候应该选择使用断言呢?
- 断言失败是致命的、 不可恢复的错误
- 断言检查只用于开发和测试阶段
为文档假设使用断言
很多程序员使用注释说明假设条件:
if (i % 3 == 0)
...
else if (i % 3 = 1)
...
else // (i % 3 == 2)
...
在这个示例中,使用断言会更好一些:
if (i % 3 == 0)
...
else if (i % 3 = 1)
...
else {
assert i % 3 == 2;
}
如果再仔细地考虑一下这个问题会发现一个更有意思的内容。i%3 会产生什么结果? 如果 i 是正值, 那余数肯定是 0、 1 或 2 。 如果 i 是负值, 则余数则可以是-1 和 -2 。然而,实际上都认为 i 是非负值, 因此, 最好在 if 语句之前使用下列断言:
assert i >= 0;
记录曰志
JDK 自带的日志框架已经落后,因此跳过
调试技巧
-
可以用方法打印或记录任意变量的值,使用
System.out.println
或日志打印出来 -
是在每一个类中放置一个单独的 main 方法。这样就可以对每一个类进行单元测试
-
使用 JUnit 单元测试框架
-
日志代理(logging proxy) ,使用匿名内部类,重写父类方法:
Random generator = new Random() { public double nextDouble() { double result = super.nextDouble(); // 日志记录 Logger.getGlobal().info("nextDouble: " + result); return result; } };
-
利用
Throwable
类提供的printStackTmce
方法,可以从任何一个异常对象中获得堆栈情况。不一定要通过捕获异常来生成堆栈轨迹。只要在代码的任何位置插入下面这条语句就可以获得堆栈轨迹:Thread.dumpStack();
-
—般来说, 堆栈轨迹显示在 System.err 上。也可以利用 printStackTrace(PrintWriter s) 方法将它发送到一个文件中。另外, 如果想记录或显示堆栈轨迹, 就可以采用下面的方式,将它捕获到一个字符串中:
StringWriter out = new StringWriter(); new Throwable().printStackTrace(new PrintWriter(out)); String description = out.toString();
-
将一个程序中的错误信息保存在一个文件中,错误信息被发送到
System.err
中,而不是System.out
中捕获错误流: java MyProgram 2> errors.txt 在同一个文件中同时捕获 System.err 和 System.out java MyProgram 1> errors.txt 2>&1
-
让非捕获异常的堆栈轨迹出现在 System.err 中并不是一个很理想的方法。比较好的方式是将这些内容记录到一个文件中。可以调用静态的
Thread.setDefaultUncaughtExceptionHandler
方法改变非捕获异常的处理器:Thread.setDefaultUncaughtExceptionHandler(new Thread.UncaughtExceptionHandler() { @Override public void uncaughtException(Thread t, Throwable e) { // ... } });
-
要想观察类的加载过程, 可以用
-verbose
标志启动 Java 虚拟机 -
-Xlint
选项告诉编译器对一些普遍容易出现的代码问题进行检査javac -Xlint:fallthrough
-
Java 虚拟机增加了对 Java 应用程序进行监控(monitoring) 和管理 (management ) 的支持。 例如 jconsole
jps # 728 是 pid jconsole 728
-
使用 jmap 实用工具获得一个堆的转储, 其中显示了堆中的每个对象
jps # ddd 是导出文件的名称,728 是 pid jmap -dump:format=b,file=ddd 728 jhat ddd # 访问 http://localhost:7000/
-
如果使用
-Xprof
标志运行 Java 虚拟机, 就会运行一个基本的剖析器来跟踪那些代码中经常被调用的方法。剖析信息将发送给 System.out 。输出结果中还会显示哪些方法是由即时编译器编译的# 得到所有非标准选项的列表 java -X # 增加 VM 参数 -Xprof