Java-5-异常处理与调试
Java 的异常处理机制 通过在程序遇到问题的地方 转交程序控制权予对应的 handler,可以提升程序的鲁棒性。本篇将详细介绍相关的语法与处理过程。此外,本篇还介绍 调试 java 程序的两个工具,断言(assert) 和 日志(logging) 工具。
A. 异常处理
Java 的异常抛出(throw) 和 捕获(catch)机制,通过可分离于 主程序之外的 异常处理代码来处理异常,从而可以有效的 将 异常检测(发生于业务程序中)和 异常处理进行解耦,从而可以提升了程序的灵活性。
a. 异常的分类
异常都继承了 Throwable
类。具体划分参考下图:

Error 通常是无法处理的异常。例如存储耗尽等问题, 通常由于外部不可控因素引起。
Exception 部分,可分为 unchecked exceptions 和 checked exceptions。
Unchecked exceptions(上图中的 RuntimeException) 描述的是 程序执行中 的逻辑错误,例如 NullPointerException
。这类异常通常具有不可预知性,故通常不需要做 catch 处理。
而 Checked exceptions 是可以预期的,例如 IOException
。对于这种类型的异常,我们需要使用与异常原因相符的异常对象来处理。
1 | // 例子程序,输出 输入两数之间的一个随机数 |
java 提供了很多的异常类,但当我们需要处理的特定异常不包括在其中的话,我们可以通过继承已有异常类 来 创建自己的异常类,例子如下:
1 | public class FileFormatException extends IOException{ |
b. 声明异常
对于 我们可以预判可能出现的 Checked Exceptions,通常在 在函数的头部 声明这个 异常。例子如下:
1 | public void wirte(Object obj, String filename) |
需要注意的是,当方式 是一个 重写(override) 方法的时候,它只能抛出超类中声明抛出的异常类 (也就是说 子类中 抛出的异常类型 不能 超出超类中声明的异常类型)。
此外,可以在 注释 中 使用 @throws
标签对异常进行说明。
c. 捕获异常
使用下面的 try ... catch ...
语句 捕获异常。
1 | try{ |
具体的,在程序执行 statements 语句的时候,如果遇到了 异常,就会将程序控制权 交给 handler,ExceptionClass 则是 handler 可以检视的 异常对象类型。
上面的 catch
语句按顺序执行,因此,在最上面的 Exception
需要最具体。
d. 涉及资源管理的异常处理
1- try
中处理
对于一些需要调用资源进行操作的代码,如果在中途发生异常,程序执行权切换,可能引起 所调用资源的错误关闭/无法关闭,从而引发系列问题。
因此,我们使用 下述的 try … catch … 代码处理需要进行资源调用的 代码:
1 | try(PrintWriter out = new PrintWriter("Output.txt")){ |
上述代码中,在 try
的小括号打开相关资源。要注意的是,所打开的资源 所属的类需要实现 AutoCloseable
接口,并重写其中的 close()
方法。
上述代码避免资源关闭异常的具体过程为: 在执行过程中 无论 try 中的内容是顺利执行完毕还是由于 异常中途退出 try 代码块,都会在退出的时候调用 close()
方法,从而确保资源得到关闭。
try 中允许打开多个资源, 在括号中使用分号分割,关闭时按照逆序关闭(最后打开的先关闭)。
当可能触发多个异常的时候,使用 Exception 对象的 getSuppressed()
方法 获得 第一个异常外的第二个异常。Throwable[] secodaryExceptions = ex.getSuppressed();
(ex 为 Exception 对象).
2- finally
语句
当我们需要清理非 AutoCloseable
的对象的时候,(例如 获取、释放 程序锁,增加、较小计数器计数值,出入栈操作等) 我们可以使用 finally 语句,如下:
1 | try{ |
finally 代码块中的内容 在 try 代码块中的内容结束时执行(正常结束 or 由于异常中途结束。如果发生异常,则在 catch 语句之后执行)。
需要注意的是:
- finally 语句中应该避免 写可能抛出异常的 语句;
- finally 语句中 不应该包含
return
语句,因为 如果 finally 中带有return
语句,会覆盖 try 中的return
语句,是的 try 中的return
不会执行。(关于try-cathc-finally中return的详解)
e. 异常的 非catch 处理与调试方式
1- 异常的再抛出与链式异常(Chained Exceptions)
当我们遇到一个异常但是无法处理,我们想 log 错误日志后 再次抛出异常,可以使用如下语句.
1 | try{ |
如果我们想 改变 抛出异常的 种类 (例如将异常转为函数声明处所声明的异常),可以利用 链式异常 的机制,将原异常的 信息保存在 新生成的异常 中,最终抛出 新生成的异常。并且也可以通过所生成的异常找到原始的异常。 参考例子如下:
1 | // 例子1:提供带有 “原因异常” 参数的 构造函数 的异常 |
2- 未捕获的异常与栈追踪(stack trace)
当程序遇到没有被捕获的异常时, 系统会 输出一个 stack trace, 即程序异常点 的 方法调用 栈 的列表。具体的,该列表会被发送到 System.err
, 然后由这一信息流对象输出 stack trace 信息。
当然,当我们 catch 一个异常时,也可以输出异常发生处的 stack trace,例子如下:
1 | try{ |
3- 方便的空指针异常检测技术
Objects 类提供了一个方便的 检测对象是否为 null 的方法 - requireNonNull
,例子如下:
1 | public void process(String direction){ |
如果被检查的 对象 direction
为 null
则会抛出一个 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 | assert conditon; |
也就是当 上面 的 condition 判断为 false 时,第一句,直接抛出 AssertionError
, 第二个 则是在 抛出 AssertionError
的同时 将 expression 转为 字符串 作为 message 传给 error 对象。
java 的 assertions 机制可以在 执行 class 的时候通过命令参数 使能/去使能, e.g.
1 | // 使能 assertions 机制,-ea 或者 -ebableassertions |
C. 日志
logging API 的目的是提供一个更加 方便,更加强大的 调试工具,来替代使用 System.out.println()
进行调试分析。
Java 的logging 系统 位于 java.util.logging 包中,此外,除了 java 标准的 logging 系统,也有很多第三方的 logging 分析包。
默认的全局 logger: 最简单的使用 logging 系统 的方法是调用全局的 默认 logger,如下:
1 | Logger.getGlobal().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 | // 通过 .等级名 设置 log 等级 |
配合Exception使用的 log:两个 Logger 的方法 logger.log()
, logger.throwing()
, 可以输出异常相关的信息。
Logging配置文件:可以通过修改位于 jre/lib/logging.properties
的 logging 配置文件 来修改 logging 系统的一些特性,例如 默认日志等级 等。
Log handler: 默认的,logger 会将日志记录发送给 ConsoleHandler
, 然后由其输出到 System.err
。logger 的handler 也有等级,我们同样可以在日志文件中设置输出等级的阈值。此外,我们也可以构建自己的 log handler。