—- 来源: Java核心技术 卷1 基础知识(第十版)—-

在理想状态下,用户输人数据的格式永远都是正确的, 选择打开的文件也一定存在,并且永远不会出现 bug。然而,在现实世界中却充满了不良的数据和带有问题的代码。

处理错误

假设在一个 Java 程序运行期间出现了一个错误。这个错误可能是由于文件包含了错误信息,或者网络连接出现问题造成的,也有可能是因为使用无效的数组下标, 或者试图使用一个没有被赋值的对象引用而造成的。用户期望在出现错误时, 程序能够采用一些理智的行
为。如果由于出现错误而使得某些操作没有完成, 程序应该:
1.返回到一种安全状态,并能够让用户执行一些其他的命令
2.允许用户保存所有操作的结果,并以妥善的方式终止程序

用户输入错误

除了那些不可避免的键盘输人错误外, 有些用户喜欢各行其是, 不遵守程序的要求。例如, 假设有一个用户请求连接一个 URL,而语法却不正确。在程序代码中应该对此进行检查, 如果没有检査,网络层就会给出警告。

设备错误

硬件并不总是让它做什么,它就做什么。打印机可能被关掉了。网页可能临时性地不能浏览。在一个任务的处理过程中,硬件经常出现问题。例如,打印机在打印过程中可能没有纸了。

物理限制

磁盘满了,可用存储空间已被用完。

代码错误

程序方法有可能无法正确执行。例如,方法可能返回了一个错误的答案,或者错误地调用了其他的方法。计算的数组索引不合法, 试图在散列表中查找一个不存在的记录, 或者试图让一个空找执行弹出操作,这些都属于代码错误。
对于方法中的一个错误,传统的做法是返回一个特殊的错误码, 由调用方法分析。例如,对于一个从文件中读取信息的方法来说, 返回值通常不是标准字符,而是一个-1, 表示文件结束。这种处理方式对于很多异常状况都是可行的。还有一种表示错误状况的常用返回值是 null 引用。

在 Java 中, 如果某个方法不能够采用正常的途径完整它的任务,就可以通过另外一个路径退出方法。在这种情况下,方法并不返回任何值, 而是抛出( throw) 一个封装了错误信息的对象。需要注意的是,这个方法将会立刻退出,并不返回任何值。 此外, 调用这个方法的代码也将无法继续执行,取而代之的是, 异常处理机制开始搜索能够处理这种异常状况的异常处理器 (exception handler )。

异常分类

Java 程序设计语言中, 异常对象都是派生于 Throwable 类的一个实例,Java 程序设计语言中, 异常对象都是派生于 Throwable 类的一个实例。

Java 基础 - 异常、断言和日志 - 图1

所有的异常都是由 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), 在方法的首部声明这个方法可能抛出的异常。

抛出单个异常

  1. public Image loadImage(String str) throws IOException, FileNotFoundException {
  2. //....
  3. }

抛出多个异常

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