Java-5-异常处理与调试

Java 的异常处理机制 通过在程序遇到问题的地方 转交程序控制权予对应的 handler,可以提升程序的鲁棒性。本篇将详细介绍相关的语法与处理过程。此外,本篇还介绍 调试 java 程序的两个工具,断言(assert) 和 日志(logging) 工具。

A. 异常处理

Java 的异常抛出(throw) 和 捕获(catch)机制,通过可分离于 主程序之外的 异常处理代码来处理异常,从而可以有效的 将 异常检测(发生于业务程序中)和 异常处理进行解耦,从而可以提升了程序的灵活性。

a. 异常的分类

异常都继承了 Throwable 类。具体划分参考下图:

Fig_link

Error 通常是无法处理的异常。例如存储耗尽等问题, 通常由于外部不可控因素引起。

Exception 部分,可分为 unchecked exceptions 和 checked exceptions。

Unchecked exceptions(上图中的 RuntimeException) 描述的是 程序执行中 的逻辑错误,例如 NullPointerException。这类异常通常具有不可预知性,故通常不需要做 catch 处理。

而 Checked exceptions 是可以预期的,例如 IOException。对于这种类型的异常,我们需要使用与异常原因相符的异常对象来处理。

1
2
3
4
5
6
7
8
// 例子程序,输出 输入两数之间的一个随机数
public static int randInt(int low, int high)
throws IllegalArgumentException{
return low + (int) (Math.random() * (high - low + 1));
//如果输入下限值 大于 输入上限值,抛出异常
if(low > high)
throw new IllegalArgumentException(String.format("low should be <= high but low is %d and high is %d", low, high));
}

java 提供了很多的异常类,但当我们需要处理的特定异常不包括在其中的话,我们可以通过继承已有异常类 来 创建自己的异常类,例子如下:

1
2
3
4
5
6
7
public class FileFormatException extends IOException{
//同时提供无参构造函数 和 可以接受 异常信息 介绍的 构造函数。
public FileFormatException(){}
public FileFormatException(String message){
super(message);
}
}

b. 声明异常

对于 我们可以预判可能出现的 Checked Exceptions,通常在 在函数的头部 声明这个 异常。例子如下:

1
2
3
4
public void wirte(Object obj, String filename) 
throws IOException, ReflectiveOperationException{
...
}

需要注意的是,当方式 是一个 重写(override) 方法的时候,它只能抛出超类中声明抛出的异常类 (也就是说 子类中 抛出的异常类型 不能 超出超类中声明的异常类型)。

此外,可以在 注释 中 使用 @throws 标签对异常进行说明。

c. 捕获异常

使用下面的 try ... catch ... 语句 捕获异常。

1
2
3
4
5
6
7
8
9
try{
statements
} catch(ExceptionClass1 ex){
handler1
} catch(ExceptionClass2 ex){
handler2
} catch(ExceptionClass3 ex){
handler3
}

具体的,在程序执行 statements 语句的时候,如果遇到了 异常,就会将程序控制权 交给 handler,ExceptionClass 则是 handler 可以检视的 异常对象类型。

上面的 catch 语句按顺序执行,因此,在最上面的 Exception 需要最具体。

d. 涉及资源管理的异常处理

1- try中处理

对于一些需要调用资源进行操作的代码,如果在中途发生异常,程序执行权切换,可能引起 所调用资源的错误关闭/无法关闭,从而引发系列问题。

因此,我们使用 下述的 try … catch … 代码处理需要进行资源调用的 代码:

1
2
3
4
5
6
7
8
9
10
11
try(PrintWriter out = new PrintWriter("Output.txt")){
for(String line:lines){
out.println(line.toLowerCase());
}
} //在此处调用 out.close()
//或者 try() 中的内容为一个effectively final 的 variable
PrintWriter out = new PrintWriter("Output.txt");
try(out){
for(String line : lines)
out.println(line.toLowerCase());
} //在此处调用 out.close()

上述代码中,在 try 的小括号打开相关资源。要注意的是,所打开的资源 所属的类需要实现 AutoCloseable 接口,并重写其中的 close() 方法。

上述代码避免资源关闭异常的具体过程为: 在执行过程中 无论 try 中的内容是顺利执行完毕还是由于 异常中途退出 try 代码块,都会在退出的时候调用 close() 方法,从而确保资源得到关闭。

try 中允许打开多个资源, 在括号中使用分号分割,关闭时按照逆序关闭(最后打开的先关闭)。

当可能触发多个异常的时候,使用 Exception 对象的 getSuppressed() 方法 获得 第一个异常外的第二个异常。Throwable[] secodaryExceptions = ex.getSuppressed();(ex 为 Exception 对象).

2- finally语句

当我们需要清理非 AutoCloseable 的对象的时候,(例如 获取、释放 程序锁,增加、较小计数器计数值,出入栈操作等) 我们可以使用 finally 语句,如下:

1
2
3
4
5
try{
Do work
}finally{
Clean up
}

finally 代码块中的内容 在 try 代码块中的内容结束时执行(正常结束 or 由于异常中途结束。如果发生异常,则在 catch 语句之后执行)。

需要注意的是:

  1. finally 语句中应该避免 写可能抛出异常的 语句;
  2. finally 语句中 不应该包含 return 语句,因为 如果 finally 中带有 return 语句,会覆盖 try 中的 return 语句,是的 try 中的 return 不会执行。(关于try-cathc-finally中return的详解)

e. 异常的 非catch 处理与调试方式

1- 异常的再抛出与链式异常(Chained Exceptions)

当我们遇到一个异常但是无法处理,我们想 log 错误日志后 再次抛出异常,可以使用如下语句.

1
2
3
4
5
6
try{
Do work
} catch (Exception ex){
logger.log(level, message, ex);
throw ex;
}

如果我们想 改变 抛出异常的 种类 (例如将异常转为函数声明处所声明的异常),可以利用 链式异常 的机制,将原异常的 信息保存在 新生成的异常 中,最终抛出 新生成的异常。并且也可以通过所生成的异常找到原始的异常。 参考例子如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// 例子1:提供带有 “原因异常” 参数的 构造函数 的异常
// 以数据库 SQL 异常为例子,最终要求抛出 ServletException
try {
Access the database
} catch(SQLException ex){
// 原异常的信息被传入了 ServletException 的构造函数中
throw new ServletException("database error", ex);
}
// 调用上述代码的代码 捕获到 ServletException后,
// 可以retrive 原异常
Throwable cause = ex.getCause();

// 例子2:构造函数中无法传入 “原因异常”
try {
Access the database
} catch(SQLException ex){
// 原异常的信息 通过 initCause() 方法传入新的异常中
Throwable ex2 = new CruftyOldException("database error"); //作者自己造的一个异常类..
ex2.initCause(ex);
throw ex2;
}

2- 未捕获的异常与栈追踪(stack trace)

当程序遇到没有被捕获的异常时, 系统会 输出一个 stack trace, 即程序异常点 的 方法调用 栈 的列表。具体的,该列表会被发送到 System.err, 然后由这一信息流对象输出 stack trace 信息。

当然,当我们 catch 一个异常时,也可以输出异常发生处的 stack trace,例子如下:

1
2
3
4
5
try{
Class<?> cl = Class.forName(className);
} catch(ClassNotFoundException ex){
ex.printStackTrace(); //打印栈追踪内容
}

3- 方便的空指针异常检测技术

Objects 类提供了一个方便的 检测对象是否为 null 的方法 - requireNonNull,例子如下:

1
2
3
public void process(String direction){
this.direction = Objects.requireNonNull(direction);
}

如果被检查的 对象 directionnull 则会抛出一个 NullPointerException,从而使得从 stack trace 中寻找异常原因时变得容易(从定位到的 requireNonNull() 可以迅速知道异常原因)。

此外,Objects.requireNonNullElse(direction, "North") 还可以为是 null 的对象 提供一个 默认的替代值,此外,这里还可以使用 lambda表达式 来实现延迟执行(就是只有 direction 为 null 才执行 lambda表达式 中的内容,而不像 上面直接给出 "North" ,无论 direction 是否为 null 都为生成对应字符串), Objects.requreNonNullElseGet(direction, () -> System.getProperty("com.horstmann.direction.default"));

B. 断言

断言(assertion)是 java 中一个调试工具,当 断言 中的内容为 false 时,程序会中断执行,并且抛出 AssertionError

断言 声明有如下两种形式:

1
2
assert conditon;
assert condition : expression;

也就是当 上面 的 condition 判断为 false 时,第一句,直接抛出 AssertionError, 第二个 则是在 抛出 AssertionError 的同时 将 expression 转为 字符串 作为 message 传给 error 对象。

java 的 assertions 机制可以在 执行 class 的时候通过命令参数 使能/去使能, e.g.

1
2
// 使能 assertions 机制,-ea 或者 -ebableassertions
java -ea MainClass

C. 日志

logging API 的目的是提供一个更加 方便,更加强大的 调试工具,来替代使用 System.out.println() 进行调试分析。

Java 的logging 系统 位于 java.util.logging 包中,此外,除了 java 标准的 logging 系统,也有很多第三方的 logging 分析包。

默认的全局 logger: 最简单的使用 logging 系统 的方法是调用全局的 默认 logger,如下:

1
2
Logger.getGlobal().info("Opening file " + filename);
// 输出:日期 + 时间 + 类名 + INFO: + "Opening file " + filename;

构建定制的logger对象: 除了系统的 logger,我们可以构建自己的 logger 对象,Logger logger = Logger.getLogger("com.mycompany.myapp");

Java 的 logger 有严格的等级系统,父logger 与 子logger 共享对应的性质(properties)。(例如 我们关闭 logger “com.mycompany” 的 messages 功能,子 logger 的 messages 功能也被关闭)

日志等级:总共有7种日志等级:SEVERE, WARNING, INFO, CONFIG, FINE, FINER, FINEST. 默认的,系统会输出 最前面三个等级的 日志。我们可以通过 logger.setLevel(Level.FINE) 更改输出的阈值。使用 Level.OFF 关闭所有 log 输出。

默认的, log 的等级为 INFO 级别(or higher),提供两种方法输出不同 等级的 log:

1
2
3
4
5
6
7
// 通过 .等级名 设置 log 等级
logger.warning(message);
logger.fine(message);

// 通过向 log 函数传入等级对象设置等级
Level level = ...;
logger.log(level, message);

配合Exception使用的 log:两个 Logger 的方法 logger.log(), logger.throwing(), 可以输出异常相关的信息。

Logging配置文件:可以通过修改位于 jre/lib/logging.properties 的 logging 配置文件 来修改 logging 系统的一些特性,例如 默认日志等级 等。

Log handler: 默认的,logger 会将日志记录发送给 ConsoleHandler, 然后由其输出到 System.err。logger 的handler 也有等级,我们同样可以在日志文件中设置输出等级的阈值。此外,我们也可以构建自己的 log handler。