20220424 Java核心技术 卷1 基础知识 7

异常、断言和曰志

处 理 错 误

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

异常分类

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

img

所有的异常都是由 Throwable 继承而来,但在下一层立即分解为两个分支: ErrorException

Error 类层次结构描述了 Java 运行时系统的内部错误和资源耗尽错误。 应用程序不应该抛出这种类型的对象。 如果出现了这样的内部错误, 除了通告给用户,并尽力使程序安全地终止之外, 再也无能为力了。这种情况很少出现。

在设计 Java 程序时, 需要关注 Exception 层次结构。 这个层次结构又分解为两个分支:一个分支派生于 RuntimeException ; 另一个分支包含其他异常。划分两个分支的规则是: 由程序错误导致的异常属于 RuntimeException ; 而程序本身没有问题, 但由于像 I/O 错误这类问题导致的异常属于其他异常。

派生于 RuntimeException 的异常包含下面几种情况:

  • 错误的类型转换
  • 数组访问越界
  • 访问 null 指针

不是派生于 RuntimeException 的异常包括:

  • 试图在文件尾部后面读取数据
  • 试图打开一个不存在的文件
  • 试图根据给定的字符串查找 Class 对象, 而这个字符串表示的类并不存在

“ 如果出现 RuntimeException 异常, 那么就一定是你的问题 ” 是一条相当有道理的规则。

Java 语 言 规 范 将 派 生 于 Error 类 或 RuntimeException 的所有异常称为 非受查 ( unchecked ) 异常 , 所有其他的异常称为 受查(checked ) 异常 。 编译器将核查是否为所有的受査异常提供了异常处理器。

RuntimeException 这个名字很容易让人混淆。 实际上, 现在讨论的所有错误都发生在运行时。

声明受查异常

方法应该在其首部声明所有可能抛出的异常。这样可以从首部反映出这个方法可能抛出哪类受査异常。

在自己编写方法时, 不必将所有可能抛出的异常都进行声明。至于什么时候需要在方法中用 throws 子句声明异常, 什么异常必须使用 throws 子句声明, 需要记住在遇到下面 4 种情况时应该抛出异常:

  • 调用一个抛出受査异常的方法, 例如, FilelnputStream 构造器
  • 程序运行过程中发现错误, 并且利用 throw 语句抛出一个受查异常
  • 程序出现错误, 例如,a[-l]=0 会抛出一个 ArrayIndexOutOfBoundsException 这样的非受查异常
  • Java 虚拟机和运行时库出现的内部错误

如果一个方法有可能抛出多个受查异常类型, 那么就必须在方法的首部列出所有的异常类。每个异常类之间用逗号隔开。 如下所示:

public Image loadlmage(String s) throws FileNotFoundException, EOFException 
{
    
}

不需要声明 Java 的内部错误, 即从 Error 继承的错误。 也不应该声明从 RuntimeException 继承的那些非受查异常。

如果在子类中覆盖了超类的一个方法, 子类方法中声明的受查异常不能比超类方法中声明的异常更通用 (也就是说, 子类方法中可以抛出更特定的异常, 或者根本不抛出任何异常)。

如何抛出异常

EOFException 异常描述的是 “ 在输入过程中, 遇到了一个未预期的 EOF 后的信号 ”。

对于一个已经存在的异常类, 将其抛出非常容易。在这种情况下:

  1. 找到一个合适的异常类
  2. 创建这个类的一个对象
  3. 将对象抛出

创建异常类

习惯上, 定义的类应该包含两个构造器, 一个是默认的构造器;另一个是带有详细描述信息的构造器(超类 ThrowabletoString 方法将会打印出这些详细信息, 这在调试中非常有用)。

public class FileFormatException extends IOException {
    public FileFormatException() {
    }

    public FileFormatException(String gripe) {
        super(gripe);
    }
}

捕获异常

捕获异常

要想捕获一个异常, 必须设置 try/catch 语句块。

如果在 try语句块中的任何代码抛出了一个在 catch 子句中说明的异常类, 那么

  1. 程序将跳过 try 语句块的其余代码
  2. 程序将执行 catch 子句中的处理器代码

请记住, 编译器严格地执行 throws 说明符。 如果调用了一个抛出受查异常的方法,就必须对它进行处理, 或者继续传递。

哪种方法更好呢? 通常, 应该捕获那些知道如何处理的异常, 而将那些不知道怎样处理的异常继续进行传递。

如果想传递一个异常, 就必须在方法的首部添加一个 throws 说明符, 以便告知调用者这个方法可能会抛出异常。

这个规则也有一个例外。前面曾经提到过:如果编写一个覆盖超类的方法,而这个方法又没有抛出异常(例如 Runnablerun 方法),那么这个方法就必须捕获方法代码中出现的每一个受查异常。不允许在子类的 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();  

强烈建议使用这种包装技术。这样可以让用户抛出子系统中的高级异常,而不会丢失原始异常的细节。

如果在一个方法中发生了一个受查异常, 而不允许抛出它, 那么包装技术就十分有用。我们可以捕获这个受查异常, 并将它包装成一个运行时异常。

有时你可能只想记录一个异常, 再将它重新抛出, 而不做任何改变

Java 编译器查看 catch 块中的 throw 语句, 然后查看 e 的类型,发现抛出的是 Exception ,编译器会跟踪到 e 来自 try 块。如果 try 块中的所有受查异常都在方法声明上,那么就是合法的。

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;
    }
}

finally 子句

不管是否有异常被捕获,finally 子句中的代码都被执行。

try 语句可以只有 finally 子句,而没有 catch 子句。

强烈建议解搞合 try/catch 和 try/finally 语句块。 这样可以提高代码的清晰度。

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 语句, 这个返回值将会覆盖原始的返回值。

带资源的 try 语句

假设资源属于一个实现了 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());
    }
}

带资源的 try 语句自身也可以有 catch 子句和一个 finally 子句。这些子句会在关闭资源之后执行。 不过在实际中, 一个 try 语句中加入这么多内容可能不是一个好主意。

分析堆栈轨迹元素

堆栈轨迹( stack trace ) 是一个方法调用过程的列表, 它包含了程序执行过程中方法调用的特定位置。

可以调用 Throwable 类的 printStackTrace 方法访问堆栈轨迹的文本描述信息

使用 getStackTrace 方法, 它会得到 StackTraceElement 对象的一个数组,StackTraceElement 类含有能够获得文件名和当前执行的代码行号的方法, 同时, 还含有能够获得类名和方法名的方法。

静态的 Thread.getAllStackTrace 方法, 它可以产生所有线程的堆栈轨迹 .

使用异常机制的技巧

  1. 异常处理不能代替简单的测试
  2. 不要过分地细化异常
  3. 利用异常层次结构
  4. 不要压制异常
  5. 在检测错误时,“ 苛刻 ” 要比放任更好
  6. 不要羞于传递异常

规则 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...

使用断言完成参数检查

在 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 自带的日志框架已经落后,因此跳过

调试技巧

  1. 可以用方法打印或记录任意变量的值,使用 System.out.println 或日志打印出来

  2. 是在每一个类中放置一个单独的 main 方法。这样就可以对每一个类进行单元测试

  3. 使用 JUnit 单元测试框架

  4. 日志代理(logging proxy) ,使用匿名内部类,重写父类方法:

    Random generator = new Random() {
        public double nextDouble() {
            double result = super.nextDouble();
            // 日志记录
            Logger.getGlobal().info("nextDouble: " + result);
            return result;
        }
    };
    
  5. 利用 Throwable 类提供的 printStackTmce 方法,可以从任何一个异常对象中获得堆栈情况。不一定要通过捕获异常来生成堆栈轨迹。只要在代码的任何位置插入下面这条语句就可以获得堆栈轨迹:

    Thread.dumpStack();
    
  6. —般来说, 堆栈轨迹显示在 System.err 上。也可以利用 printStackTrace(PrintWriter s) 方法将它发送到一个文件中。另外, 如果想记录或显示堆栈轨迹, 就可以采用下面的方式,将它捕获到一个字符串中:

    StringWriter out = new StringWriter();
    new Throwable().printStackTrace(new PrintWriter(out));
    String description = out.toString();
    
  7. 将一个程序中的错误信息保存在一个文件中,错误信息被发送到 System.err 中,而不是 System.out

    捕获错误流:
    java MyProgram 2> errors.txt
    在同一个文件中同时捕获 System.err 和 System.out
    java MyProgram 1> errors.txt 2>&1
    
  8. 让非捕获异常的堆栈轨迹出现在 System.err 中并不是一个很理想的方法。比较好的方式是将这些内容记录到一个文件中。可以调用静态的 Thread.setDefaultUncaughtExceptionHandler 方法改变非捕获异常的处理器:

    Thread.setDefaultUncaughtExceptionHandler(new Thread.UncaughtExceptionHandler() {
        @Override
        public void uncaughtException(Thread t, Throwable e) {
            // ...
        }
    });
    
  9. 要想观察类的加载过程, 可以用 -verbose 标志启动 Java 虚拟机

  10. -Xlint 选项告诉编译器对一些普遍容易出现的代码问题进行检査

    javac -Xlint:fallthrough
    
  11. Java 虚拟机增加了对 Java 应用程序进行监控(monitoring) 和管理 (management ) 的支持。 例如 jconsole

    jps
    # 728 是 pid
    jconsole 728
    
  12. 使用 jmap 实用工具获得一个堆的转储, 其中显示了堆中的每个对象

    jps
    # ddd 是导出文件的名称,728 是 pid
    jmap -dump:format=b,file=ddd 728
    jhat ddd
    # 访问 http://localhost:7000/
    
  13. 如果使用 -Xprof 标志运行 Java 虚拟机, 就会运行一个基本的剖析器来跟踪那些代码中经常被调用的方法。剖析信息将发送给 System.out 。输出结果中还会显示哪些方法是由即时编译器编译的

    # 得到所有非标准选项的列表
    java -X
    # 增加 VM 参数
    -Xprof
    
posted @ 2022-04-24 21:15  流星<。)#)))≦  阅读(48)  评论(0编辑  收藏  举报