—- 来源: Java核心技术 卷1 基础知识(第十版)—-
在理想状态下,用户输人数据的格式永远都是正确的, 选择打开的文件也一定存在,并且永远不会出现 bug。然而,在现实世界中却充满了不良的数据和带有问题的代码。
处理错误
假设在一个 Java 程序运行期间出现了一个错误。这个错误可能是由于文件包含了错误信息,或者网络连接出现问题造成的,也有可能是因为使用无效的数组下标, 或者试图使用一个没有被赋值的对象引用而造成的。用户期望在出现错误时, 程序能够采用一些理智的行
为。如果由于出现错误而使得某些操作没有完成, 程序应该:
1.返回到一种安全状态,并能够让用户执行一些其他的命令
2.允许用户保存所有操作的结果,并以妥善的方式终止程序
用户输入错误
除了那些不可避免的键盘输人错误外, 有些用户喜欢各行其是, 不遵守程序的要求。例如, 假设有一个用户请求连接一个 URL,而语法却不正确。在程序代码中应该对此进行检查, 如果没有检査,网络层就会给出警告。
设备错误
硬件并不总是让它做什么,它就做什么。打印机可能被关掉了。网页可能临时性地不能浏览。在一个任务的处理过程中,硬件经常出现问题。例如,打印机在打印过程中可能没有纸了。
物理限制
磁盘满了,可用存储空间已被用完。
代码错误
程序方法有可能无法正确执行。例如,方法可能返回了一个错误的答案,或者错误地调用了其他的方法。计算的数组索引不合法, 试图在散列表中查找一个不存在的记录, 或者试图让一个空找执行弹出操作,这些都属于代码错误。
对于方法中的一个错误,传统的做法是返回一个特殊的错误码, 由调用方法分析。例如,对于一个从文件中读取信息的方法来说, 返回值通常不是标准字符,而是一个-1, 表示文件结束。这种处理方式对于很多异常状况都是可行的。还有一种表示错误状况的常用返回值是 null 引用。
在 Java 中, 如果某个方法不能够采用正常的途径完整它的任务,就可以通过另外一个路径退出方法。在这种情况下,方法并不返回任何值, 而是抛出( throw) 一个封装了错误信息的对象。需要注意的是,这个方法将会立刻退出,并不返回任何值。 此外, 调用这个方法的代码也将无法继续执行,取而代之的是, 异常处理机制开始搜索能够处理这种异常状况的异常处理器 (exception handler )。
异常分类
Java 程序设计语言中, 异常对象都是派生于 Throwable 类的一个实例,Java 程序设计语言中, 异常对象都是派生于 Throwable 类的一个实例。
所有的异常都是由 Throwable 继承而来,但在下一层立即分解为两个分支:
Error 和 Exception
Error 类层次结构描述了 Java 运行时系统的内部错误和资源耗尽错误。 应用程序不应该抛出这种类型的对象。 如果出现了这样的内部错误, 除了通告给用户,并尽力使程序安全地终止之外, 再也无能为力了。这种情况很少出现。
在设计 Java 程序时, 需要关注 Exception 层次结构。 这个层次结构又分解为两个分支:一个分支派生于 RuntimeException ; 另一个分支包含其他异常。划分两个分支的规则是:
由程序错误导致的异常属于 RuntimeException:
1.错误的类型转换。
2.数组访问越界。
3.访问 null 指针。
而程序本身没有问题, 但由于像 I/O 错误这类问题导致的异常属于其他异常:
1.试图在文件尾部后面读取数据。
2.试图打开一个不存在的文件。
3.试图根据给定的字符串查找 Class 对象, 而这个字符串表示的类并不存在。
“如果出现 RuntimeException 异常, 那么就一定是你的问题” 是一条相当有道理的规则。
声明受查异常
如果遇到了无法处理的情况,那么Java 方法可以抛出一个异常。(一个方法不仅需要告诉编译器将要返回什么值,还要告诉编译器有可能发生什么错误。)
在自己编写方法时候,不必将所有可能抛出的异常都进行声明,至于什么时候需要在方法中使用throws 子句声明异常,什么异常必须要使用throws 子句声明,需要记住在遇到下面 4 种情况时应该抛出异常:
1)调用一个抛出受查异常的方法,例如:FileInputStream 构造器。
2)程序运行过程中发现错误,并利用throw 语句抛出一个受查异常。
3)程序出现错误,例如,a[-l]=0 会抛出一个 ArraylndexOutOffloundsException 这样的非受查异常。
4)Java 虚拟机和运行时库出现的内部错误。
如果出现前两种情况之一, 则必须告诉调用这个方法的程序员有可能抛出异常。 为什么? 因为任何一个抛出异常的方法都有可能是一个死亡陷阱。 如果没有处理器捕获这个异常,当前执行的线程就会结束。
对于那些可能被他人使用的 Java 方法, 应该根据异常规范(exception specification), 在方法的首部声明这个方法可能抛出的异常。
抛出单个异常
public Image loadImage(String str) throws IOException, FileNotFoundException {
//....
}
抛出多个异常
public Image loadImage(String str) throws IOException{
//...
}
总之,一个方法必须声明所有可能抛出的受查异常, 而非受查异常要么不可控制( Error),要么就应该避免发生 ( RuntimeException )。 如果方法没有声明所有可能发生的受查异常, 编译器就会发出一个错误消息。
如何抛出异常
public void a(){
//... 逻辑
throw new EOFException();
}
对于一个已经存在的异常类, 将其抛出非常容易 D 在这种情况下:
1 ) 找到一个合适的异常类。
2 ) 创建这个类的一个对象。
3 ) 将对象抛出。
一旦方法抛出了异常, 这个方法就不可能返回到调用者。也就是说, 不必为返回的默认值或错误代码担忧。
创建异常类
在程序中, 可能会遇到任何标准异常类都没有能够充分地描述清楚的问题。 在这种情况下,创建自己的异常类就是一件顺理成章的事情了。我们需要做的只是定义一个派生于Exception 的类,或者派生于 Exception 子类的类。
import java.io.IOException;
/**
* 自定义异常类
*
* @author xGuo
* @date 2022/03/30
*/
class FileFormatException extends IOException {
public FileFormatException() {
}
public FileFormatException(String message) {
super(message);
}
}
测试
public class MyTest {
public static void main(String[] args) throws FileFormatException {
int i = 1;
while (i < 5) {
i += 1;
System.out.println(i);
if (i == 4) {
throw new FileFormatException("出错啦");
}
}
}
}
输出
Exception in thread "main" com.xguo.basistest.exception.FileFormatException: 出错啦
at com.xguo.basistest.exception.MyTest.main(MyTest.java:10)
2
3
4
捕获异常
到目前为止, 已经知道如何抛出一个异常。 这个过程十分容易。只要将其抛出就不用理踩了。当然, 有些代码必须捕获异常。捕获异常需要进行周密的计划。
捕获异常
如果某个异常发生的时候没有在任何地方进行捕获,那程序就会终止执行,并在控制台上打印出异常信息, 其中包括异常的类型和堆栈的内容。
public static void main(String[] args) {
int i = 1;
try {
int k = i / 0;
}catch (ArithmeticException e){
e.printStackTrace(); // 输出 -> java.lang.ArithmeticException: / by zero at com.xguo.basistest.exception.MyTest.main(MyTest.java:7)
System.out.println(e.getMessage()); // 输出 -> / by zero
}
}
如果在 try语句块中的任何代码抛出了一个在 catch 子句中说明的异常类, 那么
1 ) 程序将跳过 try语句块的其余代码
2 ) 程序将执行 catch 子句中的处理器代码。
如果在 try 语句块中的代码没有拋出任何异常,那么程序将跳过 catch 子句。如果方法中的任何代码拋出了一个在 catch 子句中没有声明的异常类型,那么这个方法就会立刻退出(希望调用者为这种类型的异常设计了 catch 子句)。
捕获多个异常
在一个 try 语句块中可以捕获多个异常类型,并对不同类型的异常做出不同的处理。可以按照下列方式为每个异常类型使用一个单独的 catch 子句
try {
//... 代码
}catch (ArithmeticException e0){
}catch (FileNotFoundException | SQLDataException e1){
}catch (NullPointerException e2){
}
只有当捕获的异常类型彼此之间不存在子类关系时才需要这个特性。(以上仅仅只是例子)
再次抛出异常与异常链
在 catch 子句中可以抛出一个异常, 这样做的目的是改变异常的类型,: 如果开发了一个供其他程序员使用的子系统, 那么, 用于表示子系统故障的异常类型可能会产生多种解释。ServletException 就是这样一个异常类型的例子。执行 servlet 的代码可能不想知道发生错误的细节原因, 但希望明确地知道 servlet 是否有问题
下面给出了捕获异常并将它再次抛出的基本方法:
try{
// access the database
}catch (SQLException e){
throw new ServletException("database error: " + e.getMessageO) ;
}
这里,ServleException 用带有异常信息文本的构造器来构造。不过,可以有一种更好的处理方法,并且将原始异常设置为新异常的“ 原因”:
try{
// access the database
}catch (SQLException e){
Throwable se = new ServletException ("database error");
se.initCause(e);
throw se;
}
当捕获到异常时, 就可以使用下面这条语句重新得到原始异常:
Throwable e = se.getCause();
强烈建议使用这种包装技术。这样可以让用户抛出子系统中的高级异常,而不会丢失原始异常的细节。
演示
public static void main(String[] args) {
int i = 1;
try {
int k = i / 0;
}catch (ArithmeticException e){
RuntimeException se = new RuntimeException("除零导致计算异常");
se.initCause(e);
throw se;
}
}
输出
Exception in thread "main" java.lang.RuntimeException: 除零导致计算异常
at com.xguo.basistest.exception.MyTest.main(MyTest.java:14)
Caused by: java.lang.ArithmeticException: / by zero
at com.xguo.basistest.exception.MyTest.main(MyTest.java:12)
finally 子句
当代码抛出一个异常时, 就会终止方法中剩余代码的处理,并退出这个方法的执行。 如果方法获得了一些本地资源,并且只有这个方法自己知道,又如果这些资源在退出方法之前必须被回收,那么就会产生资源回收问题。
一种解决方案是捕获并重新抛出所有的异常。但是,这种解决方案比较乏味,这是因为需要在两个地方清除所分配的资源。一个在正常的代码中;另一个在异常代码中。
Java 有一种更好的解决方案, 这就是 finally 子句。不管是否有异常被捕获,finally 子句中的代码都被执行。
public static void main(String[] args) {
int i = 1;
try {
int k = i / 0;
}catch (ArithmeticException e){
RuntimeException se = new RuntimeException("除零导致计算异常");
se.initCause(e);
throw se;
}finally {
System.out.println("i = " + i);
}
}
输出
i = 1
Exception in thread "main" java.lang.RuntimeException: 除零导致计算异常
at com.xguo.basistest.exception.MyTest.main(MyTest.java:14)
Caused by: java.lang.ArithmeticException: / by zero
at com.xguo.basistest.exception.MyTest.main(MyTest.java:12)
扩展:建议解耦合try/catch 和 try/finally 语句,可以提高代码的清晰度
public static void main(String[] args) {
int i = 1;
try {
try {
int k = i / 0;
} finally {
System.out.println("i = " + i);
}
} catch (Exception e) {
e.printStackTrace();
}
}
带资源的 try 语句
带资源的 try 语句(try-with-resources) 的最简形式为:
try (Resource res = . . .)
{
work with res
}
try块退出时,会自动调用 res.close()。
分析堆栈轨迹元素
public static void main(String[] args) {
Throwable t = new Throwable();
StackTraceElement[] frames = t.getStackTrace();
for (StackTraceElement frame : frames) {
//获取类名
System.out.println("frame.getClassName() = " + frame.getClassName());
//获取方法名
System.out.println("frame.getMethodName() = " + frame.getMethodName());
//获取文件名
System.out.println("frame.getFileName() = " + frame.getFileName());
//获取行号
System.out.println("frame.getLineNumber() = " + frame.getLineNumber());
}
}
输出
frame.getClassName() = com.xguo.basistest.exception.MyTest
frame.getMethodName() = main
frame.getFileName() = MyTest.java
frame.getLineNumber() = 12
使用异常机制的技巧
异常处理不能代替简单的测试
使用异常的基本规则是:只在异常情况下使用异常机制。
不要过分地细化异常
不要将每一条语句都分装在一个独立的 try语句块中,这种编程方式将导致代码量的急剧膨胀。
利用异常层次结构
不要只抛出 RuntimeException 异常。应该寻找更加适当的子类或创建自己的异常类。
不要只捕获 Thowable 异常, 否则,会使程序代码更难读、 更难维护。
不要压制异常
如果认为异常非常重要, 就应该对它们进行处理。
在检测错误时,“ 苛刻” 要比放任更好
我们认为:在出错的地方抛出一个 EmptyStackException 异常要比在后面抛出一个 NullPointerException 异常更好。
不要羞于传递异常
其实, 传递异常要比捕获这些异常更好,让高层次的方法通知用户发生了错误, 或者放弃不成功的命令更加适宜。
使用断言
断言机制允许在测试期间向代码中插入一些检査语句。当代码发布时,这些插人的检测句将会被自动地移走。
Java 语言引人了关键字 assert。这个关键字有两种形式:
assert 条件;
assert 条件:表达式;
这两种形式都会对条件进行检测, 如果结果为 false, 则抛出一个 AssertionError 异常。在第二种形式中,表达式将被传人 AssertionError 的构造器, 并转换成一个消息字符串。
启用和禁用断言
在默认情况下, 断言被禁用。可以在运行程序时用 -enableassertions 或 -ea 选项启用:java -enableassertions MyApp 需要注意的是,在启用或禁用断言时不必重新编译程序。启用或禁用断言是类加载器( class loader) 的功能。当断言被禁用时, 类加载器将跳过断言代码, 因此,不会降低程序运行的速度。
使用断言完成参数检查
在 Java 语言中, 给出了 3 种处理系统错误的机制:
1.抛出一个异常
2.日志
3.使用断言
什么时候应该选择使用断言呢? 请记住下面几点:
1.断言失败是致命的、 不可恢复的错误。
2.断言检查只用于开发和测阶段(这种做法有时候被戏称为“ 在靠近海岸时穿上救生衣,但在海中央时就把救生衣抛掉吧”)。
因此,不应该使用断言向程序的其他部分通告发生了可恢复性的错误,或者,不应该作为程序向用户通告问题的手段。断言只应该用于在测试阶段确定程序内部的错误位置。
记录日志
记录日志API :
1.可以很容易地取消全部日志记录,或者仅仅取消某个级别的日志,而且打开和关闭这个操作也很容易。
2.可以很简单地禁止日志记录的输出, 因此,将这些日志代码留在程序中的开销很小。
3.日志记录可以被定向到不同的处理器, 用于在控制台中显示, 用于存储在文件中等。
4.日志记录器和处理器都可以对记录进行过滤。过滤器可以根据过滤实现器制定的标准丢弃那些无用的记录项。
5.日志记录可以采用不同的方式格式化,例如,纯文本或 XML。
6.应用程序可以使用多个日志记录器, 它们使用类似包名的这种具有层次结构的名字,例如, com.mycompany.myapp。
7.在默认情况下,日志系统的配置由配置文件控制。 如果需要的话, 应用程序可以替换这个配置。
基本曰志
要生成简单的日志记录,可以使用全局日志记录器(global logger) 并调用其 info 方法:
public class MyLogger {
public static void a() {
Logger.getGlobal().info("打印日志");
}
public static void main(String[] args) {
a(); // 输出 -> 三月 30, 2022 9:58:05 下午 com.xguo.basistest.log.MyLogger a 信息: 打印日志
}
}
设置日志级别
public class MyLogger {
public static void a() {
Logger.getGlobal().setLevel(Level.OFF);
Logger.getGlobal().info("打印日志");
}
public static void main(String[] args) {
a(); // 无输出
}
}
高级曰志
public class MyLogger {
private static final Logger myLogger = Logger.getLogger("com.xguo.basistest.log.MyLogger");
public static void b(){
myLogger.info("高级日志");
}
public static void main(String[] args) {
b(); // 输出 -> 三月 30, 2022 10:01:59 下午 com.xguo.basistest.log.MyLogger b 信息: 高级日志
}
}
通常, 有以下 7 个日志记录器级别:
1.SEVERE
2.WARNING
3.INFO
4.CONFIG
5.FINE
6.FINER
7.FINEST
在默认情况下,只记录前三个级别。 也可以设置其他的级別。
修改日志管理器配置
可以通过编辑配置文件来修改日志系统的各种属性。在默认情况下, 配置文件存在于: jre/lib/1ogging.properties
要想使用另一个配置文件, 就要将 java.utiUogging.config.file 特性设置为配置文件的存储位置, 并用下列命令启动应用程序:
java -Djava.util.logging.config. file-configFileMainClass