《JAVA核心技术 卷I》第七章 - 异常,断言和日志

第七章 - 异常,断言和日志

目录

1. 异常

1.1 异常的类型

  • 异常对象都是派生于Throwable类的一个类实例,Throwable下可以分为两个分支:

    • Error:Error类层次结构描述了Java运行时系统的内部错误和资源耗尽错误。如果出现了这种错误,一般除了通知用户并终止程序外无能为力
    • Exception:Exception层次结构是要重点关注的,这个层次结构又分为两个分支
      • RuntimeException:由编程错误导致的异常属于RuntimeException
        • 错误的强制类型转换
        • 数组访问越界
        • 访问null指针
      • IOException:程序本身没有问题,由于像I/O错误这类问题导致的异常
        • 试图超越文件末尾继续读取数据
        • 试图打开一个不存在的文件
        • 试图根据给定的字符串查找Class对象,而这个字符串表示的类并不存在

    Java语言规范将派生于Error类或RuntimeException类的所有异常称为非检查型 异常,所有其他的异常称为检查型 异常。不应该声明从RuntimeException继承的那些非检查型异常,这些运行时错误完全在我们的控制之中,应该多花些时间修正这些错误,而不只是声明这些错误有可能发生。

1.2 抛出异常

  • 要抛出异常,需要先新建一个异常的对象,然后再抛出,在抛出时可以添加字符串用于描述异常。
1.2.1 抛出已有的异常类

如果一个已有的异常类能满足要求,抛出这个异常只需要:

  1. 找到一个合适的异常类
  2. 创建这个类的一个对象
  3. 将对象抛出
String ReadData(Scanner in) throws EOFException
{
    //...
    while(...){
        if(!in.hasNext()){
            if(n < len){
                String gripe = "Content length:" + len + ", Received:" + n;
                throw new EOFException(gripe);
            }
        }
    }
    return s;
}

如果在子类中覆盖了超类的一个方法,子类方法中声明的检查型异常不能比超类方法中声明的异常更通用。同理,如果超类方法没有抛出任何检查型异常,子类也不能抛出任何检查型异常。

1.2.2 自定义异常类

创建自定义异常类的习惯做法是,定义一个派生于Exception或其子类的类,自定义的这个类应该包含两个构造器,一个是默认的构造器,另一个是包含详细描述信息的构造器,用于描述异常

class FileFormatException extends IOException{
    public FileFormatException() {};
    public FileFormatException(String gripe){
        super(gripe);
    }
}

1.3 捕获异常

  • 如果发生了某个异常,但没有在任何地方捕获这个异常,程序就会终止,并在控制台上打印一个消息,其中包括这个异常的类型和一个堆栈轨迹

  • 想要捕获一个异常,需要设置try-catch语句块。如果try语句块中的任何代码抛出了catch子句中指定的一个异常类,那么程序将跳过try语句块的其余代码并执行catch子句中的处理器代码。如果try语句块中的代码没有抛出任何异常,那么程序将跳过catch子句。如果方法中的任何代码抛出了catch子句中没有声明的一个异常类型,那么这个方法就会立刻退出。

  • 一个try语句块中可以捕获多个异常类型

    try{
    
    }
    catch(FileNotFoundException e){
        
    }
    catch(UnknownHostException e){
        
    }
    catch(IOException e){
        
    }
    

    异常对象可能包含有关异常性质的信息。要想获得这个对象的更多信息,可以尝试使用e.getMessage()得到详细的错误信息,或者使用e.getClass().getName()

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

    InputStream in = ...;
    try{
        try{
            //...
        }
        finally{
            in.close();
        }
    }
    catch(IOException e){
        
    }
    
    //内层的try语句块只有一个职责,就是确保关闭输入流。外层的try语句块也只有一个指责,就是确保报告出现的错误。这种结构的语句功能更强,结构更清晰
    
  • 当finally子句包含return语句时,有可能发生意外。假设利用return语句从try语句块中退出。在方法return前,就会执行finally子句块。如果finally块也有一个return语句,这个返回值将会屏蔽原来的返回值。总之,finally子句的体主要用于清理资源,不要把改变控制流的语句(return,throw,break,continue)放在finally子句中

1.3.1 堆栈轨迹
  • 堆栈轨迹(stack trace)是程序执行过程中某个特定点上所有挂起的方法调用的一个列表。当Java程序因为一个未捕获的异常而终止时,就会显示堆栈轨迹。

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

    var t = new Throwable();
    var out = new StringWriter();
    t.printStackTrace(new PrintWriter(out));
    String description = out.toString();
    

1.4 try-with-Resources语句

  • try块正常退出时,或者存在一个异常时,都会调用in.close()方法,就好像使用了finally块一样

    //还可以指定多个资源
    try(Scanner in = new Scanner(
    	new FileInputStream("/user/share/..."),StandardCharset.UTF-8);
       	PrintWriter out = new PrintWriter("out.txt",StandardCharsets.UTF-8))
    {
        while(in.hasNext())
            System.out.println(in.next().toUpperCase());
    }
    

1.5 使用异常的技巧

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

    捕获异常所花费的时间大大超过了前者,最好只在异常情况下使用异常

  2. 不要过分的细化异常

    有必要将整个任务包在一个try语句块中,这样当任何一个操作出现问题时,就可以取消整个任务

  3. 充分利用异常层次结构

    不要只抛出RuntimeException异常。应该寻找一个合适的子类或创建自己的异常类。不要只捕获Throwable异常,否则这会使得代码更难度,更难维护

  4. 不要压制异常

    如果要throw一个发生概率极小异常,那么可能会导致编译器对调用了该方法的所有方法报错。因此可以捕获,但不处理这个异常,以此关闭这个异常的目的。不过这样以来即便异常发生了也不会被记录,要做好承担后果的准备

  5. 早抛出,晚捕获

    最好在出错的地方抛出一个合适的异常,而不是返回一个值;也没必要捕获并处理所有的异常,适当的传递可以让程序更有效率

2. 断言

  • 断言机制允许在测试期间向代码中插入一些检查,而在生产代码中会自动删除致谢检查

  • 使用断言有两种方式

    1. assert [condition]:condition为要被测试的语句
    2. assert [condition] : [expression]:expression将传入AssertionError对象的构造器,并转换成一个消息字符串。表达式的唯一目的就是产生一个消息字符串,除此之外它不具备任何功能,Java设计者有意如此以防止在断言中进行异常处理操作。

    两个语句都会计算条件,如果结果为false,则抛出一个AssertError异常

  • 断言的应用场景十分有限,它只应该用于在测试阶段测试程序内部错误的位置

3. 日志

  • 日志就像是程序测试时插入的println语句,但是相比之下日志更加规范且功能强大

3.1 创建日志

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

    Logger.getGlobal.info("File->Open menu item selected");
    //在默认情况下会打印以下记录
    //十一月 05, 2021 6:35:40 下午 Test main
    //信息: File->Open menu item selected
    
    Logger.getGlobal().setlevel(Level.OFF);
    //这条指令将会取消掉所有日志
    
  • 也可以自定义自己的日志记录器

    //可以调用getLogger方法创建或获取日志记录器
    private static final Logger myLogger = Logger.getLogger("com.mycompany.myapp");
    

    未被任何变量引用的日志记录器可能会被垃圾回收,为了防止这种情况发生,最好用静态变量储存日志记录器的引用

3.2 日志级别

  • 与包名类似,日志记录器名也有层次结构,而且层次性更强。日志记录器的父与子之间将共享某些属性,例如,如果对日志记录器"com.mycompany"设置了日志级别,它的子日志记录器也会继承这个级别。通常有以下七个日志级别:

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

    默认情况下,只记录前3个级别。如有需要可以手动设置记录的级别

    logger.setLevel(Level.FINE);
    //现在FINE及所有更高级别的日志都会记录
    //此外可以使用Level.ALL开启所有级别的日志记录,使用Level.OFF关闭所有级别的日志记录
    
    //可以对同一日志的不同级别添加信息
    logger.warning(message);
    logger.fine(message);
    
    //还可以使用log方法指定添加信息的级别
    logger.log(Level.FINE,message);
    

3.3 日志常用方法

  • 用于跟踪执行流的方法

    //这些调用将生成FINIER级别而且以字符串ENTRY和RETURN开头的日志记录
    void entering(String className, String methodName);
    void entering(String className, String methodName,Object param);
    void entering(String className, String methodName,Object param[]);
    
    void exiting(String className,String methodName);
    void exiting(String className, String methodName, Object result);
    
  • 记录异常的方法

    //throwing调用可以记录一条FINER级别的日志记录和一条以THROW开始的消息
    void throwing(String className, String methodName,Throwable t);
    void log(Level l,String message,Throwable t);
    

3.4 处理器,过滤器,格式化器

  • 本笔记不记录关于处理器,过滤器,格式化器的配置或扩展相关知识,仅记录最基础的相关概念。相关详细内容见书P309~313
3.4.1 处理器
  • 日志记录器会将记录发送到父处理器,而最终的祖先处理器有一个ConsoleHandler,由它将记录输出到System.err流。对于一个要记录的日志记录,它的日志级别必须高于日志记录器和处理器二者的阈值。默认的控制台处理器的日志级别为INFO

  • 要想将日志记录发送到其它地方,就要添加其它的处理器。API为此提供了两个处理器,FileHandler和SocketHandler。SocketHandler可以将记录发送到指定的主机和端口;Filehandler可以将记录收集到文件中

    FileHandler handler = new FileHandler();
    logger.addHandler(handler);
    

    这些记录会被发送到用户主目录的javan.log文件中,默认情况下记录会格式化为XML

3.4.2 过滤器
  • 在默认情况下,会根据日志记录的级别进行过滤。每个日志记录器和处理器都有一个可选的过滤器来完成附加的过滤。要定义一个过滤器,需要实现Filter接口并定义以下方法:

    boolean isLoggable(LogRecord record);
    

    要想将过滤器安装到一个日志记录器或处理器中,只需要调用setFilter方法就可以了。同一时刻最多只能有一个过滤器

3.4.3 格式化器
  • ConsoleHandler类和FileHandler类可以生成文本和XML格式的日志记录。但也可以通过扩展Formatter类并覆盖下列方法来自定义格式

    String format(LogRecord record);
    

    可以调用setFormatter方法将格式化器安装到处理器中

4. 调试技巧

  1. 可以使用下面的方法打印和记录任意变量的值

    System.out.println("x=" + x);
    //或
    Logger.getGlobal().info("x=" + x);
    

    如果x是一个数值,则会被转换成字符串。如果x是一个对象,那么Java会调用这个对象的toSring方法(这就是为什么在自定义类中写toString这么重要,它可以提供有效信息)

    如果还想获得隐式参数对象的状态,可以打印this对象的状态

    Logger.getGlobal().info("this=" + this);
    
  2. 可以在每一个类中放置一个单独的main方法,这样就可以提供一个单元测试桩(stub),能够独立测试类

    public class MyClass{
        //类主体
        public static void main(String[] args){
            //测试代码
        }
    }
    

    可以建立一些对象,然后调用所有的方法,检查每个方法能否争取的完成工作。可以保留这些main方法,在运行应用程序的时候,Java虚拟机只调用启动类的main方法。

  3. 利用Throwable类的printStackTrace方法,可以从任意的异常对象获得堆栈轨迹,下面的代码可以捕获任意的异常,答应这个异常对象和堆栈轨迹,然后重新抛出异常,以便找到对应的处理器

    try{
        //...
    }
    catch(Throwable t){
        t.printStackTrace();
        throw t;
    }
    

    也可以在代码的某个位置插入下面这条语句就可以获得堆栈轨迹:Thread.dumpStack();

  4. 一般来说,堆栈轨迹显示在System.err上。如果想要记录或显示堆栈轨迹,可以将它捕获到一个字符串中

    StringWriter out = new StringWriter();
    new Throwable().printStackTrace(new PrintWriter(out));
    String description = out.toString();
    
  5. 在System.err中显示未捕获的异常的堆栈轨迹并不好。最好将这些信息记录到一个文件中,可以使用静态方法Thread.setDefaultUncaughtExceptionHandler改变未捕获异常的处理器

    Thread.setDefaultUncaughtExceptionHandler(
    	new Thread.UncaughtExceptionHandler(){
            public void uncaughtException(Thread t,Throwable e){
              //保存到文件中  
            };
        });
    
上一篇:python字典的setdefault方法和get方法


下一篇:python之dic {字典}(重要指数*****)