Java教程

《Java 核心卷1》ch 7 异常、断言和日志

本文主要是介绍《Java 核心卷1》ch 7 异常、断言和日志,对大家解决编程问题具有一定的参考价值,需要的程序猿们随着小编来一起学习吧!

@author:posper

@version:v 1.0

@date:2021/7/3-2021/7/4

ch 7 异常、断言和日志

  • ch 7 异常、断言和日志
    • 7.1 异常
      • 异常继承结构
      • Java 处理异常的方式(2种)
      • 抛出异常
      • 捕获异常
        • try - catch 语句
        • try - catch - finally 语句
        • try - with - resources 语句
      • 创建异常类
      • 使用异常的技巧
    • 7.2 断言
      • 断言的概念
      • 断言的使用时机
    • 7.3 日志
      • 使用 日志 API 的原因
      • 基本日志
      • 高级日志
      • 处理器
      • 过滤器
      • 格式化器
      • 日志技巧
    • 调试技巧

本文档为第二遍看 《Java 卷1》整理,第一遍看的时候画的思维导图。

ch 7 异常、断言和日志

7.1 异常

  • 异常处理的任务:就是将控制权从错误产生的地方转移给能够处理这种情况的错误处理器。

  • 在 Java 中,如果某个方法无法正常执行并采用了异常处理机制,则该方法将会立刻退出,并不返回正常值(任何值),而是抛出(throw)一个封装了错误信息的对象。然后,该方法出错地方之后的代码将不会再执行,此时异常处理机制开始搜索能够处理这样异常状况的异常处理器(exception handler)。

异常继承结构

异常继承结构

  • Java 中,所有的异常对象都是都是派生于 Throwable 类的一个类实例。

    • Throwable 分为两个分支:Error 和 Exception
  • Error

    • Error 类层次结构描述了 Java 运行时系统的内部错误资源耗尽错误
    • Error 开发程序员不用管,不能抛出这用类型的对象,JVM 直接报错退出
  • Exception

    • Exception 有分为两个分支:RuntimeException 和 其他异常(如,IOException)
    • RuntimeException(运行时异常)
      • 由编程出错的异常,属于 RuntimeException;
      • RuntimeException 编译器不会报错,但程序运行时出错;
      • 常见的运行时异常,eg:数组越界、空指针异常、错误的强制类型转换等…
    • 其他异常
      • 如果程序本身没有问题,由于像 I/0 这类问题导致的异常称为其他异常。
      • 常见的其他异常,eg:试图打开不存在的文件、试图超越文件末尾继续读取数据等…
    • 编译时异常
      • 编译时异常必须在程序编写阶段预先处理,如果不处理编译器报错,因此得名;
      • 但是,编译时异常其实仍仍发生在运行阶段。只有 run 程序之后,才能知道
  • 检查型异常 & 非检查型异常

    • 非检查型异常:派生于 Error 类或 RuntimeException 类的所有异常称为非检查型异常;
      • 程序员不用处理此类异常
        • Error 这个是错误,JVM gg 了,程序员管不着;
        • RuntimeException 发生概率较低,而且这类异常肯定是程序代码出错了(比如,空指针、数组越界等),通过修改错误代码就能使程序正确运行。如果这类异常还要处理,那程序员不得累死…
    • 检查型异常:其他的异常,称为检查型异常。
      • 需要程序员进行异常处理(2种方法,如下)
        • 抛出异常
        • 捕获异常

    “如果出现RuntimeException 异常, 那么就一定是你的问题”。此时,不应该抛出/捕获异常,而是通过修改自己的代码来使得程序正常运行。比如,出现 NullPointerException 时,不应该抛出该类异常,而是应该在变量只用前判断其是否为 null。

Java 处理异常的方式(2种)

  • 抛出异常

    • throws 关键字
  • 捕获异常

    • 使用 try … catch 捕获异常

      • 也可以使用 try … catch … finally

        • !!重点:看下try … catch … finally 执行过程。《Java卷1》第11版 p291
      • try … with … resources

        • 更常用、更方便
    • 上一级不知道这个异常(相当于自己扛下所有)

    • 举个栗子:相当于账目亏空,自掏腰包处理,瞒着上级

  • 如何选择抛出异常还是捕获异常?

    • 捕获那些已经知道如何处理的异常,抛出那些不知道怎样处理的异常

抛出异常

  • throws 关键字

  • 在方法声明的位置上(方法首部),使用 throws 关键字,抛出所有可能的检查型异常;否则,编译器报错。

  • 注意

    • 如果在子类中覆盖了超类的一个方法,子类方法中声明的检查型异常不能比超类方法中声明的异常更通用
      • 即,子类方法中可以抛出更特定的异常, 或者根本不抛出任何异常;
      • 如果超类方法没有抛出任何检查型异常, 子类也不能抛出任何检查型异常
  • 抛出异常的步骤(3步):

    1. 找到一个合适的异常类,在方法首部 throws 之;
    2. 创建这个类的一个对象;
    3. 将对象抛出。

    一旦方法抛出了异常, 这个方法就不可能返回到调用者。也就是说, 不必建立默认值的返回或错误代码。

// 抛出异常
String readData(Scanner in) throws EOFException { // 1、找到一个合适的异常类,在方法首部 throws 之;
	while (…) {
		if (in.hasNext()) { // EOF encountered
			if (n < len) {
				throw new EOFException(); // 2、创建这个类的一个对象;
                						  // 3、将对象抛出。(这里这两部合并了)
            }
        }
    }
}
  • Java 中,只要抛出异常了当前地方就不用管了。
  • Java 中抛出异常的执行过程:
    • 在方法声明的位置上,使用 throws 关键字,将异常抛给上一级(相当于甩锅给调用者);
      • 此时,上一级依然有两种处理异常的方法(即,捕获异常 或 抛出异常)
    • 如果上级调用者一直选择抛出异常,则异常最终抛给了 main 方法;
    • main 方法如果继续向上抛,最终抛给调用者 JVM(最终接盘侠)。
      • JVM 知道这个异常发生之后,只有一个结果,那便是终止程序执行。
  • 举个栗子:
    • 相当于账目亏空,扔给领导处理。
    • 领导还有两种处理办法:即自掏腰包(捕获异常),或者领导再上报他的领导(抛出异常);
    • 最终,如果一直上抛的话会被抛给大领导(main方法),他仍然有两种选择:
      • 如果大领导继续上抛,最终抛给 CEO (JVM),此时 CEO 自然没法再甩锅(上抛),能处理则处理;
      • 无法处理,那公司就破产了(程序终止)。

捕获异常

  • 如果某个异常发生的时候没有在任何地方进行捕获,那程序就会终止执行。
    • 并在控制台上打印出异常信息, 其中包括异常的类型和堆栈的内容。
    • 很自然地,想到上面那个栗子,账目亏空了,都没人管(没有任何地方进行捕获),都想跑路,那公司铁定破产(程序终止)

try - catch 语句

  • 语法

    try {
    	code
        more code
    } catch (ExceptionType e) {
    	handler for this type
    }
    
  • try - catch 语句执行顺序:

    • 如果在 try 语句块中的任何代码抛出了一个在 catch 子句中说明的异常类, 那么:
      • 1)程序将跳过 try 语句块的其余代码;
      • 2)程序将执行 catch 子句中的处理器代码。
    • 如果在 try 语句块中的代码没有拋出任何异常,那么程序将跳过catch 子句;
    • 如果方法中的任何代码拋出了一个在 catch 子句中没有声明的异常类型:
      • 那么这个方法就会立刻退出(希望方法的调用者为这种类型的异常提供了 catch 子句)。
  • 捕获多个异常

    • 在 Java 7 中,同一个 catch 子句中可以捕获多个异常类型。

      catch (FileNotFoundException | UnknownHostException e) {
          ...
      }
      
      // 等价于
      catch (FileNotFoundException e) {
          ...
      } catch (UnknownHostException e) {
          ...
      }
      
    • 只有当捕获的异常类型彼此之间不存在子类关系时才需要这个特性。

    • 捕获多个异常时, 异常变量隐含为 final 变量;

    • 捕获多个异常时,生成的字节码只包一个对应公共 catch 子句的代码。

  • 再次抛出异常

    • 在catch 子句中可以抛出一个异常, 这样做的目的是改变异常的类型。
  • 如何选择使用抛出异常/捕获异常?

    • 通常,应该捕获那些知道如何处理的异常, 而将那些不知道怎样处理的异常继续进行传递(抛出异常)。

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

  • 注意

    • 通常,将异常交给胜任的处理器进行处理比压制这个异常要好。即,通常抛出异常就可以“溜之大吉”了。
    • 但是,这个规则有个“例外”:
      • 在子类覆盖父类的方法中,如果父类没有抛出异常,就只能捕获检查型异常,而不能抛出。
      • 原因:见“抛出异常”中的“注意”:子类方法中声明的检查型异常不能比超类方法中声明的异常更通用

try - catch - finally 语句

  • 语法:

    try {
        // 1
    	code that might throw exceptions
    	// 2
    } catch (IOException e) {
    	// 3
    	show error message
    	// 4
    } finally { // 关闭资源
    	// 5
        in.dose();
    }
    // 6
    
  • finally 语句:

    • 不管异常是否被捕获,finally 子句中的代码都会执行;
    • 通常,在 finally 子句中编写一些关闭资源的代码。
  • try - catch - finally 语句执行顺序:(3 种情况)

    1. 代码没有抛出异常。
      • 此时,首先执行 try 中所有代码,然后执行 finally 语句中的所有代码。最后,执行 finally 语句块后面的第一条语句。
      • 执行顺序为:1、2、5、6
    2. 代码抛出异常,并在一个 catch 子句中捕获。(此时,又分为两种情况)
      • 此时,首先执行 try 中代码,直到抛出异常为止,此时跳过 try 语句块中剩余的代码。
      • 然后去匹配 catch 子句(此时,分为两种情况),最后执行 finally 语句中的所有代码。
        • 如果 catch 子句没有抛出异常, 程序将执行 try 语句块之后的第一条语句;
          • 执行顺序:1、3、4、5、6
        • 如果 catch 子句抛出了一个异常,异常将被抛回到这个方法的调用者。
          • 执行顺序:1、3、5
    3. 代码抛出异常,但没有在任何一个 catch 子句中捕获。
      • 此时,首先执行 try 中代码,直到抛出异常为止,此时跳过 try 语句块中剩余的代码。
      • 然后,执行 finally 语句中的所有代码,并将异常抛回给这个方法的调用者。
      • 执行顺序:1、5

可以只有 try - finally.

  • 注意:不要把改变控制流的语句(return,throw,break,continue)放在 finally 子句中。

    • 当 finally 子句包含 return 语句时,可能会造成方法无法返回正常值。
    • 如果 try 语句块中欧 return 语句,但在方法真正返回前会执行 finally 子句块。此时,如果 finally 块中也有一个 return 语句,这个返回值会覆盖掉原来 try 中的返回值。
    public static int test() {
    	try {
    		return 123;
    	} finally {
    		return 0; // error
            		  // 会覆盖 try 中合法的 return 结果
    	}
    }
    

try - with - resources 语句

  • Java 7 之后,支持 try - with - resources 语句,比 finall 子句更常用、更方便。

  • 语法:

    try (Resource res = ...) {
        work with res
    }
    
    • try 子句块退出后,会自动调用 res.close()。不用再在 finally 中写了…
  • Java 9 中,可以在 try 首部中提供之前声明的事实最终变量

    Resource res = ...
    try (res) { // 事实最终变量
    	...
    }
    

创建异常类

  • **适用情形:**可能会遇到任何标准异常类都无法充分地描述清楚的问题,此时可以自定义创建异常类。
  • 创建自己的异常类的步骤:(2 步)
    1. 定义一个派生于 Exception 的类, 或者派生于 Exception 子类的类;
    2. 定义的类应该包含两个构造器:
      • 一个是默认的无参构造器;
      • 另一个是带有详细描述信息的构造器。
    3. 抛出/捕获自定义的异常类型
// 创建异常类
class FileFormatException extends IOException { // 1、定义一个派生于 Exception 子类的类
    // 2、定义两个构造器
    // 1)无参构造器
	public FileFormatException() {}
	// 2)带有详细描述信息的构造器
	public FileFormatException(String gripe) {
		super(gripe);
	}
}

// 3、抛出自定义异常类型
String readData(BufferedReader in) throws FileFormatException { 
	while (. . .) {
		if (ch == -1) { // EOF encountered
            if (n < len) {
                throw new FileFormatException();
            }
            ...
        }
        return s;
    }
}

使用异常的技巧

  1. 异常处理不能代替简单的测试

    • 捕获异常的时间远远大于简单测试的时间
  2. 不要过分地细化异常

    • 不要一条语句放一个 try 语句,应将整个任务包在一个 try 语句块中
  3. 充分利用异常层次结构

    • 应该寻找一个适合的异常子类或者创建自定的异常类
  4. 不要压制异常

    • 如果当前方法调用的方法会抛出异常,则在当前方法中要适当处理这个异常(捕获/抛出异常)
  5. 在检测错误时,“ 苛刻” 要比放任更好

  6. 不要羞于传递异常

    • 如果调用一个抛出异常的方法,最后继续传递这个异常,而不是自己捕获。
    • 但是,当父类方法中没有抛出异常时,子类方法也不能抛出异常。

规则 5、6 归纳为“早抛出,晚捕获”。

7.2 断言

断言的概念

  • 断言机制允许在测试期间向代码中插入一些检査语句。当代码发布时,这些插入的检测语句将会被自动删除

  • 断言语法:

    • assert 关键字
    • 两种语法形式:
      • assert 条件;
      • assert 条件:表达式;
        • 表达式会传入 AssertionError 对象的构造器,并转换成一个消息字符串。
        • ”表达式“ 的唯一目的是:产生一个消息字符串。
  • Java 中处理系统错误的 3 中机制:

    1. 抛出一个异常

    2. 日志

    3. 使用断言

断言的使用时机

  • 断言注意事项(2点)

    1. 断言失败是致命的、不可恢复的错误

    2. 断言检查只用于开发和测阶段

  • 断言只应该用于在测试阶段确定程序内部的错误位置

断言是一种测试和调试阶段所使用的战术性工具; 而日志记录是一种在程序的整个生命周期都可以使用的策略性工具。

7.3 日志

使用 日志 API 的原因

  • 如果在程序中一直使用 System.out.println(mess) 打印日志的话,一会开一会关就会很不方便。
  • 使用日志 API 的优点:
    • 可以很轻松地 打开/取消整个或者全部日志;
    • 可以对日志记录进行过滤;
    • 可以采用不同的方式进行日志记录格式化;
    • 日志记录由配置文件控制
    • 等等等… 《Java 卷1》p304

基本日志

  • 生成简单的日志时,可以使用全局日志记录器,并调用其 info 方法。

高级日志

  • 自定义日志记录器

    • 使用 getLogger 方法
    // 未被任何变量引用的日志记录器可能会被垃圾回收,所以用一个静态变量存储日志记录器的一个引用。
    private static final Logger myLogger = Logger.getLogger("com.mycompany.myapp");
    
  • 可以修改日志级别(7 个)

    • SEVERE
    • WARNING
    • INFO
    • CONFIG
    • FINE
    • FINER
    • FINEST

    默认只记录前 3 个级别,也可以利用 setLevel 方法设置不同的级别。

    logger.setLevel(Level.FINE);
    
  • 子日志记录器会继承父日志记录器的日志级别

  • 记录日志常见用途是:使用日志记录那些不可预料的异常

    • throwing 方法
    • log 方法
    // 1、throwing 方法
    if (...) {
        Exception e = new IOException("...");
        logger.throwing("com.mylib.Reader", "read", e); // 使用日志记录异常的描述
        throw e;
    }
    
    // 2、log 方法
    try {
        ...
    } catch (IOException e) {
        Logger.getLogger("com.myapp").log(Level.WARNING, "Reading image", e); // 使用日志记录异常的描述
    }
    

处理器

  • 在默认情况下日志记录器将记录发送到 ConsoleHandler 中, 并由它输出 System.err流中。
  • 处理器也有日志记录级别
  • 对于一个要被记录的日志记录,它的日志记录级别必须高于日志记录器和处理器日志界别的阈值。

过滤器

  • 默认是按照日志记录的级别进行过滤

  • 也可以自定义过滤器,来过滤掉不想要的日志记录

    • 具体步骤:(2 步)

      1. 定义一个过滤器,实现 Filter 接口

      2. 重写 isLoggable 方法

注意:同一个时刻最多只能有一个过滤器。

格式化器

  • ConsoleHandler 类和 FileHandler 类可以生成 XML 格式的日志记录。

  • 可以自定义日志记录的格式:

    • 具体步骤:(2 步)

      1. 扩展 Formatter 类

      2. 重写 format 方法

日志技巧

  1. 为一个简单的应用程序, 选择一个日志记录器,并把日志记录器命名为与主应用程序包一样的名字

  2. 默认的日志配置将级别等于或高于 INFO 级别的所有消息记录到控制台

  3. 现在,可以记录自己想要的内容了

    • 所有级别为 INFO、WARNING 和 SEVERE 的消息都将显示到控制台上;
    • 最好只将对程序用户有意义的消息设置为这几个级别;
    • 将程序员想要的日志记录,设定为 FINE 是一个很好的选择。

调试技巧

  1. 打印变量的值

    • 方法1:System.out.println(“x=” + x);
    • 方法2:Logger.getClobal().info("x = " + x);
  2. 单元测试

    • 每个类中放一个 main 方法,用来测试这个类
  3. 日志代理

    • 不知道是啥玩意,代理是 ch 6的,但是没看…
  4. 打印异常对象和堆栈轨迹

    • 利用 Throwable 类提供的 printStackTrace 方法,可以从任意的异常对象获得堆栈轨迹。
  5. …告辞,还有好多老夫没看…

    • 参考 《Java 核心卷1》第 11 版 p323
这篇关于《Java 核心卷1》ch 7 异常、断言和日志的文章就介绍到这儿,希望我们推荐的文章对大家有所帮助,也希望大家多多支持为之网!