20230530 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 种情况时应该抛出异常:
- 调用一个抛出检查型异常的方法, 例如,
FileInputStream
构造器 - 检测到一个错误,并且利用
throw
语句抛出一个检查型异常 - 程序出现错误, 例如,
a[-l]=0
会抛出一个ArrayIndexOutOfBoundsException
这样的非检查型异常 - Java 虚拟机和运行时库出现的内部错误
有些Java方法包含在对外提供的类中,对于这些方法,应该通过方法首部的异常规范(exception specification)声明这个方法可能抛出异常
如果一个方法有可能抛出多个检查型异常类型, 那么就必须在方法的首部列出所有的异常类。每个异常类之间用逗号隔开。 如下所示:
public Image loadlmage(String s) throws FileNotFoundException, EOFException
{
}
不需要声明 Java 的内部错误, 即从 Error
继承的错误。 也不应该声明从 RuntimeException
继承的那些非检查型异常。
如果在子类中覆盖了超类的一个方法, 子类方法中声明的检查型异常不能比超类方法中声明的异常更通用 (也就是说, 子类方法中可以抛出更特定的异常, 或者根本不抛出任何异常)。
如何抛出异常
EOFException
异常描述的是 “ 在输入过程中, 遇到了一个未预期的 EOF 后的信号 ”。
对于一个已经存在的异常类, 将其抛出非常容易。在这种情况下:
- 找到一个合适的异常类
- 创建这个类的一个对象
- 将对象抛出
throw new EOFException();
创建异常类
习惯上, 定义的类应该包含两个构造器, 一个是默认的构造器;另一个是带有详细描述信息的构造器(超类 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();
强烈建议使用这种包装技术。这样可以让用户抛出子系统中的高级异常,而不会丢失原始异常的细节。
如果在一个方法中发生了一个检查型异常, 而不允许抛出它, 那么包装技术就十分有用。我们可以捕获这个检查型异常, 并将它包装成一个运行时异常。
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;
}
}
上面代码中抛出了范围更大的异常,但是 Java 编译器查看 catch 块中的 throw
语句, 然后查看 e 的类型,发现抛出的是 Exception
,编译器会跟踪到 e 来自 try 块。如果 try 块中的所有检查型异常都在方法声明上,那么就是合法的。
finally 子句
不管是否有异常被捕获,finally 子句中的代码都被执行。
try 语句可以只有 finally 子句,而没有 catch 子句。
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 子句的体要用于清理资源。不要把改变控制流的语句(return, throw, break, continue)放在 finally 子句中
try-with-Resources
假设资源属于一个实现了 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());
}
}
在 Java 9 中,可以在 try 首部提供之前声明的事实最终变量:
public static void printAll(String[] lines, PrintWriter out) {
try (out) {
for (String line : lines) {
out.println(line);
}
}
}
try-with-resources 语句自身也可以有 catch 子句和一个 finally 子句。这些子句会在关闭资源之后执行。 不过在实际中, 一个 try 语句中加入这么多内容可能不是一个好主意。
分析栈轨迹元素
栈轨迹( stack trace ) 是程序执行过程中某个特定点上所有挂起的方法调用的一个列表。
可以调用 Throwable
类的 printStackTrace
方法访问栈轨迹的文本描述信息
更灵活的方法是使用 StackWalker
类,它会生成 StackWalker.StackFrame
实例流,其中每个实例分别描述一个栈帧(stack frame)
使用 getStackTrace
方法, 它会得到 StackTraceElement
对象的一个数组,StackTraceElement
类含有能够获得文件名和当前执行的代码行号的方法, 同时, 还含有能够获得类名和方法名的方法。不过,这个调用的效率不高,因为要得到整个栈,另外它只允许访问挂起方法的类名,而不能访问类对象。
静态的 Thread.getAllStackTrace
方法, 它可以产生所有线程的栈轨迹
使用异常机制的技巧
- 异常处理不能代替简单的测试
- 不要过分地细化异常
- 合理利用异常层次结构
- 不要压制异常
- 在检测错误时,“ 苛刻 ” 要比放任更好
- 不要羞于传递异常
- 使用标准方法报告 null 指针和越界异常
使用 Objects 里的方法 - 不要向最终用户显示栈轨迹
规则 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...
使用 JUnit 默认开启断言
@Test
void testAssert() {
int x=3;
assert x == 3;
assert x == 2 : "is not 3";
}
使用断言完成参数检查
在 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
类提供的printStackTrace
方法,可以从任何一个异常对象中获得堆栈情况。也不一定要通过捕获异常来生成栈轨迹,只要在代码的任何位置插入下面这条语句就可以获得栈轨迹: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
-
Java 任务控制器(Java Mission Control)是一个专业级性能分析和诊断工具,可以关联到正在运行的虚拟机,还能分析 Java 飞行记录器(Java Flight Recorder)的输出。
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】凌霞软件回馈社区,博客园 & 1Panel & Halo 联合会员上线
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】博客园社区专享云产品让利特惠,阿里云新客6.5折上折
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 一个费力不讨好的项目,让我损失了近一半的绩效!
· 清华大学推出第四讲使用 DeepSeek + DeepResearch 让科研像聊天一样简单!
· 实操Deepseek接入个人知识库
· CSnakes vs Python.NET:高效嵌入与灵活互通的跨语言方案对比
· 易语言 —— 开山篇
2020-01-13 20200113 SpringBoot整合MyBatis