zoukankan      html  css  js  c++  java
  • Java核心技术卷阅读随笔--第7章【异常、断言和日志】

    异常、断言和日志

      在理想状态下,用户输入数据的格式永远都是正确的, 选择打开的文件也一定存在,并 且永远不会出现 bug。迄今为止,本书呈现给大家的代码似乎都处在这样一个理想境界中。 然而,在现实世界中却充满了不良的数据和带有问题的代码,现在是讨论 Java 程序设计语言处理这些问题的机制的时候了。

      人们在遇到错误时会感觉不爽。如果一个用户在运行程序期间,由于程序的错误或一些 外部环境的影响造成用户数据的丢失,用户就有可能不再使用这个程序了, 为了避免这类事 情的发生, 至少应该做到以下几点:

      • 向用户通告错误;

      • 保存所有的工作结果;

      • 允许用户以妥善的形式退出程序。

      对于异常情况, 例如, 可能造成程序崩溃的错误输入,Java 使 用 一 种 称 为 异 常 处 理 ( exception handing) 的错误捕获机制处理。Java 中的异常处理与 C++ 或 Delphi 中的异常处理 十分类似。本章的第 1 部分先介绍 Java 的异常。

      在测试期间, 需要进行大量的检测以验证程序操作的正确性。 然而,这些检测可能非常耗 时,在测试完成后也不必保留它们,因此,可以将这些检测删掉, 并在其他测试需要时将它们粘贴回来,这是一件很乏味的事情。本章的第 2 部分将介绍如何使用断言来有选择地启用检测。

      当程序出现错误时,并不总是能够与用户或终端进行沟通。此时,可能希望记录下出现 的问题,以备日后进行分析。本章的第 3 部分将讨论标准 Java 日志框架。

    7.1 处理错误

      假设在一个 Java 程序运行期间出现了一个错误。这个错误可能是由于文件包含了错误 信息,或者网络连接出现问题造成的,也有可能是因为使用无效的数组下标, 或者试图使用 一个没有被赋值的对象引用而造成的。用户期望在出现错误时, 程序能够采用一些理智的行 为。如果由于出现错误而使得某些操作没有完成, 程序应该:

      • 返回到一种安全状态,并能够让用户执行一些其他的命令;或者

      • 允许用户保存所有操作的结果,并以妥善的方式终止程序。

      要做到这些并不是一件很容易的事情。其原因是检测(或引发)错误条件的代码通常离 那些能够让数据恢复到安全状态, 或者能够保存用户的操作结果, 并正常地退出程序的代码很远。异常处理的任务就是将控制权从错误产生的地方转移给能够处理这种情况的错误处理器。为了能够在程序中处理异常情况, 必须研究程序中可能会出现的错误和问题, 以及哪类问题需要关注。

      1. 用户输入错误

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

      2. 设备错误

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

      3. 物理限制

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

      4. 代码错误

      程序方法有可能无法正确执行。例如,方法可能返回了一个错误的答案,或者错误地调 用了其他的方法。计算的数组索引不合法, 试图在散列表中查找一个不存在的记录, 或者试 图让一个空找执行弹出操作,这些都属于代码错误。

      对于方法中的一个错误,传统的做法是返回一个特殊的错误码, 由调用方法分析。例 如,对于一个从文件中读取信息的方法来说, 返回值通常不是标准字符,而是一个-1 , 表示 文件结束。这种处理方式对于很多异常状况都是可行的。还有一种表示错误状况的常用返回 值是 null 引用。

      遗憾的是,并不是在任何情况下都能够返回一个错误码。有可能无法明确地将有效数据 与无效数据加以区分。一个返回整型的方法就不能简单地通过返回-1 表示错误,因为-1 很 可能是一个完全合法的结果。

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

      异常具有自己的语法和特定的继承结构。下面首先介绍一下语法, 然后再给出有效地使 用这种语言功能的技巧。

      7.1.1 异常分类

        在 Java 程序设计语言中, 异常对象都是派生于 Throwable 类的一个实例。稍后还可以看 到,如果 Java 中内置的异常类不能够满足需求,用户可以创建自己的异常类

        需要注意的是,所有的异常都是由 Throwable 继承而来,但在下一层立即分解为两个分 支:Error 和 Exception。

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

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

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

        • 错误的类型转换。

        • 数组访问越界。

        • 访问 null 指针。

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

        • 试图在文件尾部后面读取数据。

        • 试图打开一个不存在的文件。

        • 试图根据给定的字符串查找 Class 对象, 而这个字符串表示的类并不存在。

        “ 如果出现 RuntimeException 异常, 那么就一定是你的问题” 是一条相当有道理的规则。 应该通过检测数组下标是否越界来避免 ArraylndexOutOfBoundsException 异常;应该通过在 使用变量之前检测是否为 null 来杜绝 NullPointerException 异常的发生。

        如何处理不存在的文件呢? 难道不能先检查文件是否存在再打开它吗? 嗯, 这个文件有 可能在你检查它是否存在之前就已经被删除了。因此,“ 是否存在” 取决于环境,而不只是 取决于你的代码。

        Java 语 言 规 范 将 派 生 于 Error 类 或 RuntimeException 类的所有异常称为非受查 ( unchecked ) 异常,所有其他的异常称为受查( checked) 异常。这是两个很有用的术语,在 后面还会用到。 编译器将核查是否为所有的受査异常提供了异常处理器。

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

        C++ 注释:如果熟悉标准 C++ 类库中的异常层次结构, 就一定会感到有些困惑。C++ 有 两个基本的异常类, 一个是 runtime_error ; 另一个是 logic_error。logic_error 类相当于 Java 中的 RuntimeException, 它表示程今中的逻辑错误;runtime_error 类是所有由于不 可预测的原因所引发的异常的基类。它相当于 Java 中的非 RuntimeException 异常。

      7.1.2 声明受查异常

        如果遇到了无法处理的情况, 那么 Java 的方法可以抛出一个异常。这个道理很简单:一 个方法不仅需要告诉编译器将要返回什么值,还要告诉编译器有可能发生什么错误。例如, 一段读取文件的代码知道有可能读取的文件不存在, 或者内容为空,因此, 试图处理文件信 息的代码就需要通知编译器可能会抛出 IOException 类的异常。

        方法应该在其首部声明所有可能抛出的异常。这样可以从首部反映出这个方法可能抛出哪类受査异常。例如,下面是标准类库中提供的 FilelnputStream 类的一个构造器的声明(有 关输入和输出的更多信息请参看卷 2 的第 2 章。)

    public Fi1elnputStream(String name) throws FileNotFoundException

        这个声明表示这个构造器将根据给定的 String 参数产生一个 FilelnputStream 对象,但也 有可能抛出一个 FileNotFoundException 异常。如果发生了这种糟糕情况, 构造器将不会初始 化一个新的 FilelnputStream 对象, 而是抛出一个 FileNotFoundException 类对象。 如果这个方法真的抛出了这样一个异常对象,运行时系统就会开始搜索异常处理器, 以便知道如何处理 FileNotFoundException 对象。

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

        1 ) 调用一个抛出受査异常的方法, 例如, FilelnputStream 构造器。

        2 ) 程序运行过程中发现错误, 并且利用 throw语句抛出一个受查异常(下一节将详细地 介绍 throw 语句)。

        3 ) 程序出现错误, 例如,a[-1] =0 会抛出一个 ArraylndexOutOfBoundsException 这样的 非受查异常。

        4 ) Java 虚拟机和运行时库出现的内部错误。

        如果出现前两种情况之一, 则必须告诉调用这个方法的程序员有可能抛出异常。 为什么? 因为任何一个抛出异常的方法都有可能是一个死亡陷阱。 如果没有处理器捕获这个异 常,当前执行的线程就会结束

        对于那些可能被他人使用的 Java 方法, 应该根据异常规范( exception specification), 在 方法的首部声明这个方法可能抛出的异常。

    class MyAnimation
    {
        ...
        public Image loadImage(String s) throws IOException
        {
            ...
        }
    }

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

    class MyAnimation
    {
        ...
        public Image loadImage(String s) throws FileNotFoundException,EOFException
        {
            ...
        }
    }

        但是, 不需要声明 Java 的内部错误,即从 Error 继承的错误。任何程序代码都具有抛出那些 异常的潜能, 而我们对其没有任何控制能力。

        同样,也不应该声明从 RuntimeException 继承的那些非受查异常

    class MyAnimation
    {
        void drawlmage(int i) throws ArrayIndexOutOfBoundsException // bad style
        {
            ...
        }
    }

        这些运行时错误完全在我们的控制之下。如果特别关注数组下标引发的错误,就应该将 更多的时间花费在修正程序中的错误上,而不是说明这些错误发生的可能性上

        总之,一个方法必须声明所有可能抛出的受查异常, 而非受查异常要么不可控制( Error), 要么就应该避免发生( RuntimeException)。如果方法没有声明所有可能发生的受查异常, 编译器就会发出一个错误消息。

        当然, 从前面的示例中可以知道:除了声明异常之外, 还可以捕获异常。这样会使异常 不被抛到方法之外,也不需要 throws 规范。稍后,将会讨论如何决定一个异常是被捕获,还是被抛出让其他的处理器进行处理。

        警告: 如果在子类中覆盖了超类的一个方法, 子类方法中声明的受查异常不能比超类方 法中声明的异常更通用 (也就是说, 子类方法中可以抛出更特定的异常, 或者根本不抛出任何异常)。特别需要说明的是, 如果超类方法没有抛出任何受查异常, 子类也不能抛出任何受查异常。例如, 如果覆盖 JComponent.paintComponent 方法, 由于超类中这个方 法没有抛出任何异常,所以, 自定义的 paintComponent 也不能抛出任何受查异常。

        如果类中的一个方法声明将会抛出一个异常, 而这个异常是某个特定类的实例时, 则这个方法就有可能抛出一个这个类的异常, 或者这个类的任意一个子类的异常。 例 如,FilelnputStream 构造器声明将有可能抛出一个 IOExcetion 异常, 然而并不知道具体 是哪种 IOException 异常。它既可能是 IOException 异常,也可能是其子类的异常, 例如, FileNotFoundException。

        C++ 注释:Java 中的 throws 说明符与 C++ 中的 throw 说明符基本类似,但有一点重要的 区别。在 C++ 中,throw 说明符在运行时执行, 而不是在编译时执行。也就是说, C++ 编译器将不处理任何异常规范。但是, 如果函数抛出的异常没有出现在 throw 列表中, 就会调用 unexpected 函数, 这个函数的默认处理方式是终止程序的执行。

        另外,在 C++ 中, 如果没有给出 throw 说明, 函数可能会抛出任何异常。而在 Java 中, 没有 throws 说明符的方法将不能抛出任何受查异常

      7.1.3 如何抛出异常

        假设在程序代码中发生了一些很糟糕的事情。 一个名为 readData 的方法正在读取一个首部具有下列信息的文件:

    Content-length: 1024

        然而,读到 733 个字符之后文件就结束了。我们认为这是一种不正常的情况,希望抛出一个 异常。

        首先要决定应该抛出什么类型的异常。将上述异常归结为 IOException 是一种很好的选 择。仔细地阅读 Java API 文档之后会发现:EOFException 异常描述的是“ 在输入过程中, 遇 到了一个未预期的 EOF 后的信号”。这正是我们要抛出的异常。下面是抛出这个异常的语句:

    throw new EOFException();

        或者

    EOFException e = new EOFException();
    throw e;

        下面将这些代码放在一起:

    String readData(Scanner in) throws EOFException
    {
        ...
        while (...)
        {
            if (!in.hasNext()) // EOF encountered
            {
                if(n<len)
                    throw new EOFException
            }
            ...
        }
        return s;
    }

        EOFException 类还有一个含有一个字符串型参数的构造器。 这个构造器可以更加细致的 描述异常出现的情况。

    String gripe = "Content-length: " + len + ", Received: " + n;
    throw new EOFException(gripe);

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

        1 ) 找到一个合适的异常类。

        2 ) 创建这个类的一个对象。

        3 ) 将对象抛出。

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

        C++ 注释: 在 C++ 与 Java 中, 抛出异常的过程基本相同, 只有一点微小的差别。 在 Java 中, 只能抛出 Throwable 子类的对象, 而在 C++ 中, 却可以抛出任何类型的值。

      7.1.4 创建异常类

        在程序中,可能会遇到任何标准异常类都没有能够充分地描述清楚的问题。 在这种情 况下,创建自己的异常类就是一件顺理成章的事情了。 我们需要做的只是定义一个派生于 Exception 的类,或者派生于 Exception 子类的类。例如, 定义一个派生于 IOException 的类。 习惯上, 定义的类应该包含两个构造器, 一个是默认的构造器;另一个是带有详细描述信息的构造器(超类 Throwable 的 toString 方法将会打印出这些详细信息, 这在调试中非常有用)。

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

        现在,就可以抛出自己定义的异常类型了。

    String readData(BufferedReader in) throws FileFormatException
    {
        ...
        while (...)
        {
            if (ch == -1) // EOF encountered
            {
                if (n < len)
                    throw new FileFornatException();
            }
            ...
        }
        return s;
    }

        API javaJang.Throwable 1.0

        • Throwable( )

          构造一个新的 Throwabie 对象, 这个对象没有详细的描述信息。

        • Throwable(String message )

          构造一个新的 throwabie 对象, 这个对象带有特定的详细描述信息。习惯上,所有派 生的异常类都支持一个默认的构造器和一个带有详细描述信息的构造器。

        • String getMessage( )

          获得 Throwable 对象的详细描述信息。 

    7.2 捕获异常

      到目前为止, 已经知道如何抛出一个异常。这个过程十分容易。只要将其抛出就不用理 踩了。当然, 有些代码必须捕获异常。捕获异常需要进行周密的计划。这正是下面几节要介 绍的内容。

      7.2.1 捕获异常

        如果某个异常发生的时候没有在任何地方进行捕获,那程序就会终止执行,并在控制台 上打印出异常信息, 其中包括异常的类型和堆栈的内容。对于图形界面程序(applet 和应用 程序) ,在捕获异常之后,也会打印出堆桟的信息,但程序将返回到用户界面的处理循环中 (在调试 GUI 程序时, 最好保证控制台窗口可见,并且没有被最小化)。

        要想捕获一个异常, 必须设置 try/catch语句块。最简单的 try语句块如下所示:

    try
    {
        code 
        more code 
        more code
    }
    catch (ExceptionType e)
    {
        handler for this type
    }

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

        1 ) 程序将跳过 try语句块的其余代码。

        2 ) 程序将执行 catch 子句中的处理器代码。

        如果在 try 语句块中的代码没有拋出任何异常,那么程序将跳过 catch 子句。

        如果方法中的任何代码拋出了一个在 catch 子句中没有声明的异常类型,那么这个方法 就会立刻退出(希望调用者为这种类型的异常设计了 catch 子句)。     为了演示捕获异常的处理过程, 下面给出一个读取数据的典型程序代码:

    public void read(String filename)
    {
        try
        {
            InputStream in = new FileInputStream(filename);
            int b;
            while ((b = in.read())!= -1)
            {
                process input
            }
        }
        catch (IOException exception)
        {
            exception.printStackTrace();
        }
    }

        需要注意的是,try 语句中的大多数代码都很容易理解: 读取并处理字节, 直到遇到文件结束符为止。正如在 Java API 中看到的那样, read 方法有可能拋出一个 IOException 异常。 在这种情况下,将跳出整个 while 循环,进入 catch 子旬,并生成一个栈轨迹。对于一个普通的程序来说, 这样处理异常基本上合乎情理。还有其他的选择吗?

        通常, 最好的选择是什么也不做,而是将异常传递给调用者。如果 read 方法出现了错误, 就 让 read方法的调用者去操心!如果采用这种处理方式,就必须声明这个方法可能会拋 出一个 IOException。

    public void read(String filename) throws IOException
    (
        InputStream in = new FilelInputStream(filename);
        int b;
        while ((b = in.read()) != -1)
        {
            process input
        }
    }

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

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

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

        仔细阅读一下 Java API 文档, 以便知道每个方法可能会抛出哪种异常, 然后再决定是自己处理,还是添加到 throws 列表中。对于后一种情况,也不必犹豫。将异常直接交给能够胜任的处理器进行处理要比压制对它的处理更好。

        同时请记住,这个规则也有一个例外。前面曾经提到过:如果编写一个覆盖超类的方法, 而这个方法又没有抛出异常(如 JComponent 中的 paintComponent ), 那么这个方法就必须捕 获方法代码中出现的每一个受查异常。不允许在子类的 throws 说明符中出现超过超类方法所 列出的异常类范围。

        C++ 注释:在 Java 与 C++ 中,捕获异常的方式基本相同。严格地说, 下列代码

    catch (Exception e) //]ava
    catch (Exception& e) // C++
    是一样的。

        在 Java 中,没有与 C++ 中 catch() 对应的东西。由于 Java 中的所有异常类都派生于 一个公共的超类,所以, 没有必要使用这种机制。

      7.2.2 捕获多个异常

        在一个 try 语句块中可以捕获多个异常类型,并对不同类型的异常做出不同的处理。可以按照下列方式为每个异常类型使用一个单独的 catch 子句

    try
    {
        code that might throw exceptions
    }
    catch (FileNotFoundException e)
    {
        emergency action for missing files
    }
    catch (UnknownHostException e)
    {
        emergency action for unknown hosts
    }
    catch (IOException e)
    {
        emergency actionfor all other I/O problems
    }

        异常对象可能包含与异常本身有关的信息。要想获得对象的更多信息, 可以试着使用

    e.getMessage()

        得到详细的错误信息(如果有的话) ,或者使用

    e.getClass().getName()

        得到异常对象的实际类型。

        在 Java SE 7中,同一个 catch 子句中可以捕获多个异常类型。例如,假设对应缺少文件 和未知主机异常的动作是一样的,就可以合并 catch 子句

    try
    {
        code that might throw exceptions
    }
    catch (FileNotFoundException | UnknownHostException e)
    {
        emergency action for missing files and unknown hosts
    }
    catch (IOException e)
    {
        emergency actionfor all other I/O problems
    }

         只有当捕获的异常类型彼此之间不存在子类关系时才需要这个特性。

        注释: 捕获多个异常时, 异常变量隐含为 final 变量。例如,不能在以下子句体中为 e 赋 不同的值:

    catch (FileNotFoundException | UnknownHostException e) { . . . }

        注释:捕获多个异常不仅会让你的代码看起来更简单, 还会更高效。 生成的字节码只包含一个对应公共 catch 子句的代码块。

      7.2.3 再次抛出异常与异常链

        在 catch 子句中可以抛出一个异常,这样做的目的是改变异常的类型, 如果开发了一个 供其他程序员使用的子系统, 那么,用于表示子系统故障的异常类型可能会产生多种解释。 ServletException 就是这样一个异常类型的例子。执行 servlet 的代码可能不想知道发生错误的 细节原因, 但希望明确地知道 servlet 是否有问题

        下面给出了捕获异常并将它再次抛出的基本方法:

    try
    {
        access the database
    }
    catch (SQLException e)
    {
        throw new ServletException("database error: " + e.getMessage());
    }

        这里,ServleException 用带有异常信息文本的构造器来构造。

        不过,可以有一种更好的处理方法,并且将原始异常设置为新异常的“ 原因”

    try
    {
        access the database
    }
    catch (SQLException e)
    {
        Throwable se = new ServletException ("database error");
        se.initCause(e);
        throw se;
    }

        当捕获到异常时, 就可以使用下面这条语句重新得到原始异常

    Throwable e = se.getCause() ;

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

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

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

    try
    {
        access the database
    }
    catch (Exception e)
    {
        logger.log(level, message, e);
        throw e;
    }

        在 Java SE 7之前,这种方法存在一个问题。假设这个代码在以下方法中:

    public void updateRecord() throws SQLException

        Java 编译器查看 catch 块中的 throw 语句, 然后查看 e 的类型,会指出这个方法可以抛 出任何 Exception 而不只是 SQLException。现在这个问题已经有所改进。 编译器会跟踪到 e 来自 try块。假设这个 try块中仅有的已检査异常是 SQLException 实例, 另外, 假设 e 在 catch 块中未改变, 将外围方法声明为 throws SQLException 就是合法的。

      7.2.4 finally 子句

        当代码抛出一个异常时, 就会终止方法中剩余代码的处理,并退出这个方法的执行。如果方法获得了一些本地资源,并且只有这个方法自己知道,又如果这些资源在退出方法之前 必须被回收,那么就会产生资源回收问题。一种解决方案是捕获并重新抛出所有的异常。但 是,这种解决方案比较乏味,这是因为需要在两个地方清除所分配的资源。一个在正常的代 码中;另一个在异常代码中。

        Java 有一种更好的解决方案,这就是 finally 子句。下面将介绍 Java 中如何恰当地关闭 一个文件。如果使用 Java 编写数据库程序,就需要使用同样的技术关闭与数据库的连接。在 卷 2 的第 4 章中可以看到更加详细的介绍。当发生异常时,恰当地关闭所有数据库的连接是 非常重要的。

        不管是否有异常被捕获,finally 子句中的代码都被执行。在下面的示例中, 程序将在所有情况下关闭文件。

    InputStream in = new FileInputStream(. . .);
    try
    {
        //1
        code that might throwe xceptions
        //2
    }
    catch (IOException e)
    {
        // 3
        show error message
        // 4
    }
    finally
    {
        // 5
        in.close();
    }
    //6

        在上面这段代码中,有下列 3 种情况会执行 finally 子句:     

        1 ) 代码没有抛出异常。 在这种情况下, 程序首先执行 try 语句块中的全部代码,然后执行 finally 子句中的代码。随后, 继续执行 try 语句块之后的第一条语句。也就是说,执行标 注的 1、 2、 5、 6 处。

        2 ) 抛出一个在 catch 子句中捕获的异常。在上面的示例中就是 IOException 异常。在这种 情况下,程序将执行 try语句块中的所有代码,直到发生异常为止。此时,将跳过 try语句块中 的剩余代码,转去执行与该异常匹配的 catch 子句中的代码, 最后执行 finally 子句中的代码。 如果 catch 子句没有抛出异常,程序将执行 try 语句块之后的第一条语句。在这里,执行 标注 1、 3、 4、5、 6 处的语句。

        如果 catch 子句抛出了一个异常, 异常将被抛回这个方法的调用者。在这里, 执行标注 1、 3、 5 处的语句。

        3 ) 代码抛出了一个异常, 但这个异常不是由 catch 子句捕获的。在这种情况下,程序将 执行 try 语句块中的所有语句,直到有异常被抛出为止。此时, 将跳过 try 语句块中的剩余代 码, 然后执行 finally 子句中的语句, 并将异常抛给这个方法的调用者。在这里, 执行标注 1、 5 处的语句。

        try 语句可以只有 finally 子句,而没有 catch 子句。例如,下面这条 try 语句:

    InputStream in = ...;
    try
    {
      code that might throwexceptions
    }
    finally
    {
      in.close();
    }

        无论在 try 语句块中是否遇到异常,finally 子句中的 in.close() 语句都会被执行。当然, 如果真的遇到一个异常,这个异常将会被重新抛出,并且必须由另一个 catch 子句捕获。

        事实上, 我们认为在需要关闭资源时, 用这种方式使用 finally 子句是一种不错的选择。下面的提示将给出具体的解释。

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

    InputStrean in = . . .;
    try
    {
      try
      {
        code that might throwexceptions
      }
      finally
      {
        in.close();
      }
    }
    catch (IOException e)
    {
      show error message
    }

        内层的 try语句块只有一个职责, 就是确保关闭输入流。外层的 try 语句块也只有一个职 责, 就是确保报告出现的错误。这种设计方式不仅清楚, 而且还具有一个功能,就是将会报告 finally 子句中出现的错误。

        警告: finally 子句包含 return 语句时, 将会出现一种意想不到的结果„ 假设利用 return 语句从 try语句块中退出。在方法返回前, finally 子句的内容将被执行。如果 finally 子句中 也有一个 return 语句,这个返回值将会覆盖原始的返回值。请看一个复杂的例子:

    public static int f(int n)
    {
      try
      {
        int r = n * n;
        return r;
      }
      finally
      {
        if (n = 2) return 0;
      }
    }

        如果调用 f(2), 那么 try 语句块的计算结果为 r = 4, 并执行 return 语句。然而,在方法真 正返回前,还要执行 finally 子句。finally 子句将使得方法返回 0, 这个返回值覆盖了原 始的返回值 4。

        有时候, finally 子句也会带来麻烦。例如, 清理资源的方法也有可能抛出异常。假设希 望能够确保在流处理代码中遇到异常时将流关闭。

    InputStreai in = ...;
    try
    {
        code that might throw exceptions
    }
    finally
    {
      in.close();
    }

        现在,假设在 try 语句块中的代码抛出了一些非 IOException 的异常,这些异常只有这个 方法的调用者才能够给予处理。执行 finally 语句块,并调用 close 方法。而 close 方法本身也 有可能抛出 IOException 异常。当出现这种情况时, 原始的异常将会丢失,转而抛出 close 方 法的异常

        这会有问题, 因为第一个异常很可能更有意思。如果你想做适当的处理,重新抛出原来 的异常, 代码会变得极其繁琐。 如下所示:

    InputStream in = ...;
    Exception ex = null;
    try
    {
        try
        {
            code that might throw exceptions
        }
        catch (Exception e)
        {
            throw e;
        }
    }
    finally
    {
        try
        {
            in.close();
        }
        catch (Exception e)
        {
            if (ex = null) throw e;
        }
    }

        幸运的是,下一节你将了解到,Java SE 7中关闭资源的处理会容易得多。

      7.2.5 带资源的 try 语句

        对于以下代码模式:

    open a resource
    try
    {
        work with the resource
    }
    finally
    {
        close the resource
    }

        假设资源属于一个实现了 AutoCloseable 接口的类,Java SE 7 为这种代码模式提供了一 个很有用的快捷方式。AutoCloseable 接口有一个方法:

    void close() throws Exception

        注释: 另外,还有一个 Closeable 接口。这是 AutoCloseable 的子接口, 也包含一个 close 方法。不过, 这个方法声明为抛出一个 IOException。

        带资源的 try 语句(try-with-resources) 的最简形式为:

    try (Resource res = . . .)
    {
      work with res
    }

        try块退出时,会自动调用 res.close()。下面给出一个典型的例子, 这里要读取一个文件 中的所有单词:

    try (Scanner in = new Scanner(new FileInputStream("/usr/share/dict/words")), "UTF-8")
    {
        while (in.hasNext())
        System.out.println(in.next());
    }

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

        还可以指定多个资源: 例如:

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

        不论这个块如何退出, in 和 out 都会关闭。如果你用常规方式手动编程,就需要两个嵌 套的 try/finally语句。

        上一节已经看到,如果 try 块抛出一个异常, 而且 close 方法也抛出一个异常,这就会带 来一个难题。 带资源的 try 语句可以很好地处理这种情况。原来的异常会重新抛出,而 close 方法抛出的异常会“ 被抑制“。 这些异常将自动捕获,并由 addSuppressed 方法增加到原来的异常。 如果对这些异常感兴趣, 可以调用 getSuppressed 方法,它会得到从 close 方法抛出并被抑制的异常列表。

        你肯定不想采用这种常规方式编程。只要需要关闭资源, 就要尽可能使用带资源的 try 语句。

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

      7.2.6 分析堆栈轨迹元素

        堆栈轨迹( stack trace ) 是一个方法调用过程的列表, 它包含了程序执行过程中方法调用 的特定位置前面已经看到过这种列表, 当 Java 程序正常终止, 而没有捕获异常时, 这个列表就会显示出来。

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

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

        一种更灵活的方法是使用 getStackTrace 方法, 它会得到 StackTraceElement 对象的一个 数组, 可以在你的程序中分析这个对象数组。例如:

    Throwable t = new Throwable() ;
    StackTraceElement[] frames = t.getStackTrace();
    for (StackTraceElement frame : frames)
      analyze frame

        StackTraceElement 类含有能够获得文件名和当前执行的代码行号的方法, 同时, 还含有能够获得类名和方法名的方法。toString 方法将产生一个格式化的字符串, 其中包含所获得的信息。

        静态的 Thread.getAllStackTrace 方法, 它可以产生所有线程的堆栈轨迹 . 下面给出使用 这个方法的具体方式:

    Map<Thread, StackTraceElement[]> map = Thread.getAl1StackTraces();
    for (Thread t : map. keySet ())
    {
        StackTraceElement[] frames = map.get(t);
        analyze frames
    }

        有关 Map 接口与线程的更加详细的信息请参看第 9 章和第 14 章

        程序清单 7-1 打印了递归阶乘函数的堆栈情况。例如, 如果计算 factorial(3), 将 会 打 印 下列内容:

    factorial (3):
    StackTraceTest.factorial (StackTraceTest.java:18)
    StackTraceTest.main (StackTraceTest.java:34)
    factorial (2):
    StackTraceTest.factorial (StackTraceTest.java:18)
    StackTraceTest.factorial (StackTraceTest.java:24)
    StackTraceTest.main (StackTraceTest.java:34)
    factorial (1):
    StackTraceTest.factorial (StackTraceTest.java:18)
    StackTraceTest.factorial (StackTraceTest.java:24)
    StackTraceTest.factorial (StackTraceTest.java:24)
    StackTraceTest.main (StackTraceTest.java:34)
    return 1
    return 2
    return 6

        API java.Iang.Throwable 1.0

        • Throwable(Throwable cause) 1.4

        • Throwable(String message, Throwable cause) 1.4

          用给定的“ 原因” 构造一个 Throwable 对象。

        • Throwable initCause(Throwable cause) 1.4

          将这个对象设置为“ 原因”。如果这个对象已经被设置为“ 原因”, 则抛出一个异常。 返回 this 引用。

        • Throwable getCause() 1.4

          获得设置为这个对象的“ 原因” 的异常对象。如果没有设置“ 原因”, 则返回 null。

        • StackTraceElement[] getStackTrace() 1.4

          获得构造这个对象时调用堆栈的跟踪。

        • void addSuppressed(Throwable t) 7

          为这个异常增加一个“ 抑制” 异常。这出现在带资源的 try语句中, 其中 t 是 close方法抛出的一个异常。

        • Throwable[] getSuppressed() 7

          得到这个异常的所有“ 抑制” 异常。一般来说,这些是带资源的 try语句中 close 方法 拋出的异常。

        API java.lang.Exception 1.0

        • Exception(Throwable cause) 1.4

        • Exception(String message, Throwable cause)

          用给定的“ 原因” 构造一个异常对象。

        API java.lang.RuntimeException 1.0

        • RuntimeException(Throwable cause) 1.4

        • RuntimeException(String message, Throwable cause) 1.4

          用给定的“ 原因” 构造一个 RuntimeException 对象。

        API java.lang.StackTraceElement 1.4

        • String getFileName()

          返回这个元素运行时对应的源文件名。如果这信息不存在, 则返回 null。

        • int getLineNumber()

          返回这个元素运行时对应的源文件行数。如果这个信息不存在,则返回 -1。

        • String getClassName()

          返回这个元素运行时对应的类的完全限定名。

        • String getMethodName()

          返回这个元素运行时对应的方法名。构造器名是<init>;静态初始化器名是<clinit>。 这里无法区分同名的重载方法。

        • boolean isNativeMethod()

          如果这个元素运行时在一个本地方法中, 则返回 true。

        • String toString()

          如果存在的话, 返回一个包含类名、方法名、 文件名和行数的格式化字符串。

    7.3 使用异常机制的技巧

      目前, 存在着大量有关如何恰当地使用异常机制的争论。有些程序员认为所有的已检查异常都很令人厌恶;还有一些程序员认为能够拋出的异常量不够。我们认为异常机制(甚至 是已检查异常)有其用武之地。下面给出使用异常机制的几个技巧。

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

      作为一个示例, 在这里编写了一段代码, 试着上百万次地对一个空栈进行退栈操作。在 实施退栈操作之前, 首先要查看栈是否为空。

    if (!s.empty()) s.pop();

      接下来,强行进行退栈操作。然后, 捕获 EmptyStackException 异常来告知我们不能这样做。

    try
    {
        s.pop();
    }
    catch (EmptyStackException e)
    {
    }

      在测试的机器上, 调用 isEmpty 的版本运行时间为 646 毫秒。捕获 EmptyStackException 的版 本运行时间为 21739 毫秒。

      可以看出,与执行简单的测试相比, 捕获异常所花费的时间大大超过了前者, 因此使用 异常的基本规则是:只在异常情况下使用异常机制

      2. 不要过分地细化异常

      很多程序员习惯将每一条语句都分装在一个独立的 try语句块中。

    PrintStream out;
    Stack s;
    for (i = 0;i < 100; i++)
    {
        try
        {
            n = s.pop();
        }
        catch (EmptyStackException e)
        {
            //stack was empty
        }
        try
        {
            out.writelnt(n);
        }
        catch (IOException e)
        {
            //problem writing to file
        }
    }

      这种编程方式将导致代码量的急剧膨胀。首先看一下这段代码所完成的任务。在这里, 希望从栈中弹出 100 个数值, 然后将它们存入一个文件中。(别考虑为什么,这只是一个“ 玩 具” 例子。)如果栈是空的, 则不会变成非空状态;如果文件出现错误, 则也很难给予排除。 出现上述问题后,这种编程方式无能为力。因此,有必要将整个任务包装在一个 try语句块中,这样, 当任何一个操作出现问题时, 整个任务都可以取消。

    try
    {
        for (i = 0; i < 100; i++)
        {
            n = s.pop() ;
            out.writelnt(n);
        }
    }
    catch (IOException e)
    {
        // problem writing to file
    }
    catch (EmptyStackException e)
    {
        //stack was empty
    }

      这段代码看起来清晰多了。这样也满足了异常处理机制的其中一个目标,将正常处理与错误处理分开。

      3. 利用异常层次结构

      不要只抛出 RuntimeException 异常。应该寻找更加适当的子类或创建自己的异常类。

      不要只捕获 Thowable 异常, 否则,会使程序代码更难读、 更难维护。

      考虑受查异常与非受查异常的区别。 已检查异常本来就很庞大,不要为逻辑错误抛出这些异常。(例如, 反射库的做法就不正确。 调用者却经常需要捕获那些早已知道不可能发生的 异常。)

      将一种异常转换成另一种更加适合的异常时不要犹豫。例如, 在解析某个文件中 的 一 个 整 数 时, 捕 获 NumberFormatException 异 常, 然 后 将 它 转 换 成 IOException 或 MySubsystemException 的子类。

      4. 不要压制异常

      在 Java 中,往往强烈地倾向关闭异常。如果编写了一个调用另一个方法的方法,而这个 方法有可能 100 年才抛出一个异常, 那么, 编译器会因为没有将这个异常列在 throws 表中产生抱怨。而没有将这个异常列在 throws 表中主要出于编译器将会对所有调用这个方法的方法 进行异常处理的考虑。因此,应该将这个异常关闭:

    public Image loadImage(String s)
    {
        try
        {
            // code that threatens to throw checked exceptions
        }
        catch (Exception e)
        {} // so there
    }

      现在,这段代码就可以通过编译了。除非发生异常,否则它将可以正常地运行。即使发生了异常也会被忽略。如果认为异常非常重要,就应该对它们进行处理。

      5. 在检测错误时,“ 苛刻 ” 要比放任更好

      当检测到错误的时候, 有些程序员担心抛出异常。在用无效的参数调用一个方法时,返 回一个虚拟的数值, 还是抛出一个异常, 哪种处理方式更好? 例如, 当栈空时,Stack.pop是 返回一个 null, 还是抛出一个异常? 我们认为:在出错的地方抛出一个 EmptyStackException 异常要比在后面抛出一个 NullPointerException 异常更好。

      6. 不要羞于传递异常

      很多程序员都感觉应该捕获抛出的全部异常。如果调用了一个抛出异常的方法,例如, FilelnputStream 构造器或 readLine 方法,这些方法就会本能地捕获这些可能产生的异常。其 实, 传递异常要比捕获这些异常更好:

    public void readStuff(String filename) throws IOException // not a sign of shame!
    {
        InputStreaa in = new FilelnputStream(filename);
    }

      让高层次的方法通知用户发生了错误, 或者放弃不成功的命令更加适宜。

      注释: 规则 5、6 可以归纳为“ 早抛出,晚捕获 "

    7.4 使用断言

      在一个具有自我保护能力的程序中, 断言很常用。在下面的小节中,你会了解如何有效 地使用断言。

      7.4.1 断言的概念

        假设确信某个属性符合要求, 并且代码的执行依赖于这个属性。例如, 需要计算

    double y = Math.sqrt(x);

        我们确信,这里的 X 是一个非负数值。原因是:X 是另外一个计算的结果,而这个结果 不可能是负值;或者 X 是一个方法的参数,而这个方法要求它的调用者只能提供一个正整数。 然而,还是希望进行检查, 以避免让“ 不是一个数” 的数值参与计算操作。当然,也可以抛 出一个异常:

    if (x < 0) throw new IllegalArgumentException("x < 0");

        但是这段代码会一直保留在程序中, 即使测试完毕也不会自动地删除。如果在程序中含 有大量的这种检查,程序运行起来会相当慢。

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

        Java 语言引入了关键字 assert。这个关键字有两种形式:

    assert 条件;
    和
    assert 条件:表达式;

        这两种形式都会对条件进行检测, 如果结果为 false, 则抛出一个 AssertionError 异常。 在第二种形式中,表达式将被传入 AssertionError 的构造器, 并转换成一个消息字符串

        注释:“ 表达式” 部分的唯一目的是产生一个消息字符串。AssertionError 对象并不存储 表达式的值, 因此, 不可能在以后得到它。正如 JDK 文档所描述的那样: 如果使用表达式的值, 就会鼓励程序员试图从断言中恢复程序的运行, 这不符合断言机制的初衷。

        要想断言x是一个非负数值, 只需要简单地使用下面这条语句

    assert x >= 0;

        或者将 x 的实际值传递给 AssertionError 对象, 从而可以在后面显示出来。

    assert x >= 0 : x

        C++ 注释: C 语言中的 assert 宏将断言中的条件转换成一个字符串。 当断言失败时, 这个字符串将会被打印出来。例如, 若 assert(x>=0) 失败, 那么将打印出失败条件 “ x>=0”。在 Java 中, 条件并不会自动地成为错误报告中的一部分。如果希望看到这个 条件, 就必须将它以字符串的形式传递给 AssertionError 对象:assert x >= 0“: x >= 0” 。

      7.4.2 启用和禁用断言

        在默认情况下, 断言被禁用。可以在运行程序时用 -enableassertions-ea 选项启用:

    java -enableassertions MyApp

        需要注意的是, 在启用或禁用断言时不必重新编译程序。启用或禁用断言是类加载器 ( class loader) 的功能。当断言被禁用时, 类加载器将跳过断言代码, 因此,不会降低程序运行的速度。

        也可以在某个类或整个包中使用断言, 例如:

    java -ea:MyClass -ea:com.mycompany.mylib... MyApp

        这条命令将开启 MyClass 类以及在 com.mycompany.mylib 包和它的子包中的所有类的断言。选项 -ea 将开启默认包中的所有类的断言

        也可以用选项 -disableassertions-da 禁用某个特定类和包的断言

    java -ea:... -da:MyClass MyApp

        有些类不是由类加载器加载, 而是直接由虚拟机加载。可以使用这些开关有选择地启用 或禁用那些类中的断言。

        然而, 启用和禁用所有断言的 -ea 和 -da 开关应用到那些没有类加载器的“ 系统类” 上。对于这些系统类来说, 需要使用 -enablesystemassertions/-esa 开关启用断言

        在程序中也可以控制类加载器的断言状态。有关这方面的内容请参看本节末尾的 API 注释。

      7.4.3 使用断言完成参数检查

        在 Java 语言中, 给出了 3 种处理系统错误的机制

        • 抛出一个异常

        • 日志

        • 使用断言

        什么时候应该选择使用断言呢? 请记住下面几点:

        • 断言失败是致命的、 不可恢复的错误。

        • 断言检查只用于开发和测阶段(这种做法有时候被戏称为“ 在靠近海岸时穿上救生衣, 但在海中央时就把救生衣抛掉吧”)。

        因此,不应该使用断言向程序的其他部分通告发生了可恢复性的错误,或者,不应该作 为程序向用户通告问题的手段。断言只应该用于在测试阶段确定程序内部的错误位置。

        下面看一个十分常见的例子:检查方法的参数。是否应该使用断言来检查非法的下标值或 null 引用呢? 要想回答这个问题, 首先阅读一下这个方法的文档。假设实现一个排序方法。

    /**
    Sorts the specified range of the specified array in ascending numerical order.
    The range to be sorted extends from fromIndex, inclusive, to toIndex, exclusive.
    @param a the array to be sorted.
    @param fromIndex the index of the first element (inclusive) to be sorted.
    @param toIndex the index of the last element (exclusive) to be sorted.
    ©throws IllegalArgumentException if fromIndex > toIndex
    ©throws ArrayIndexOutOfBoundsException if fromIndex < 0 or toIndex > a.length
    */
    static void sort(int[] a, int fromIndex, int toIndex)

        文档指出,如果方法中使用了错误的下标值, 那么就会抛出一个异常。这是方法与调用 者之间约定的处理行为。如果实现这个方法,那就必须要遵守这个约定,并抛出表示下标值 有误的异常。因此,这里使用断言不太适宜。

        是否应该断言 a 不是 null 呢? 这也不太适宜。当 a 是 null 时, 这个方法的文档没有指出 应该采取什么行动。在这种情况下, 调用者可以认为这个方法将会成功地返回,而不会抛出 一个断言错误。 然而,假设对这个方法的约定做一点微小的改动:

    @param a the array to be sorted (must not be null).

        现在,这个方法的调用者就必须注意:不允许用 null 数组调用这个方法,并在这个方法 的开头使用断言:

    assert a != null;

        计算机科学家将这种约定称为前置条件( Precondition)。最初的方法对参数没有前置条 件, 即承诺在任何条件下都能够给予正确的执行。修订后的方法有一个前置条件,即 a 非 空。如果调用者在调用这个方法时没有提供满足这个前置条件的参数, 所有的断言都会失 败,并且这个方法可以执行它想做的任何操作。事实上,由于可以使用断言,当方法被非法调用时, 将会出现难以预料的结果。有时候会拋出一个断言错误, 有时候会产生一个 null 指 针异常, 这完全取决于类加载器的配置。

      7.4.4 为文档假设使用断言

         很多程序员使用注释说明假设条件。看一下 http://docs.oracle.eom/javase/6/docs/technotes/ guides/language/assert.html 上的一个示例;

    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;

        无论如何, 这个示例说明了程序员如何使用断言来进行自我检查。前面已经知道, 断言是一种测试和调试阶段所使用的战术性工具;日志记录是一种在程序的整个生命周期都可以使用的策略性工具。下一节将介绍日志的相关知识。

        API java.Iang.ClassLoader 1.0

        • void setDefaultAssertionStatus( boolean b ) 1.4

          对于通过类加载器加载的所有类来说, 如果没有显式地说明类或包的断言状态, 就启用或禁用断言。

        • void setCIassAssertionStatus(String className , boolean b ) 1.4

          对于给定的类和它的内部类,启用或禁用断言。

        • void setPackageAssertionStatus( String packageName , boolean b ) 1.4

          对于给定包和其子包中的所有类,启用或禁用断言。

        • void clearAssertionStatus( ) 1.4

          移去所有类和包的显式断言状态设置, 并禁用所有通过这个类加载器加载的类的断言。

    7.5 记录日志

      每个 Java 程序员都很熟悉在有问题的代码中插入一些 System.out.println 方法调用来帮助 观察程序运行的操作过程。 当然, 一旦发现问题的根源, 就要将这些语句从代码中删去。如果接下来又出现了问题, 就需要再插入几个调用 System.out.println方法的语句。记录日志 API 就是为了解决这个问题而设计的。下面先讨论这些 API 的优点。

      • 可以很容易地取消全部日志记录,或者仅仅取消某个级别的日志,而且打开和关闭这个操作也很容易。

      • 可以很简单地禁止日志记录的输出, 因此,将这些日志代码留在程序中的开销很小。

      • 日志记录可以被定向到不同的处理器, 用于在控制台中显示, 用于存储在文件中等。

      • 日志记录器和处理器都可以对记录进行过滤。过滤器可以根据过滤实现器制定的标准 丢弃那些无用的记录项。

      • 日志记录可以采用不同的方式格式化,例如,纯文本或 XML。

      • 应用程序可以使用多个日志记录器, 它们使用类似包名的这种具有层次结构的名字, 例如, com.mycompany.myapp。

      • 在默认情况下,日志系统的配置由配置文件控制。如果需要的话, 应用程序可以替换 这个配置。

      

      7.5.1 基本曰志

        要生成简单的日志记录,可以使用全局日志记录器(global logger) 并调用其 info 方法:

    Logger.getClobal().info("File->Open menu item selected");

        在默认情况下,这条记录将会显示以下内容:

    May 10, 2021 10:12:15 PM LogginglmageViewer fileOpen
    INFO: File->0pen menu item selected

        但是, 如果在适当的地方(如 main 开始)调用

    Logger.getClobal ().setLevel (Level .OFF);

        将会取消所有的日志。

      7.5.2 高级曰志

        从前面已经看到“ 虚拟日志”,下面继续看一下企业级( industrial-strength) 日志。在一 个专业的应用程序中,不要将所有的日志都记录到一个全局日志记录器中,而是可以自定义日志记录器。

        可以调用 getLogger 方法创建或获取记录器:

    private static final Logger myLogger = Logger.getLogger("com.mycompany.myapp"):

        提示:未被任何变量引用的日志记录器可能会被垃圾回收为了防止这种情况发生,要像上面的例子中一样, 用一个静态变量存储日志记录器的一个引用。

        与包名类似,日志记录器名也具有层次结构。事实上, 与包名相比,日志记录器的层次性更强。 对于包来说,一个包的名字与其父包的名字之间没有语义关系,但是日志记录器 的父与子之间将共享某些属性。例如, 如果对 com.mycompany 日志记录器设置了日志级别, 它的子记录器也会继承这个级别 ,

        通常, 有以下 7 个日志记录器级别:

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

        在默认情况下,只记录前三个级别。 也可以设置其他的级別。例如,

    logger.setLevel (Level .FINE);

        现在, FINE 和更高级别的记录都可以记录下来。

        另外, 还可以使用 Level.ALL 开启所有级别的记录, 或者使用 Level.OFF 关闭所有级别 的记录。

        对于所有的级别有下面几种记录方法:

    logger.warning(message);
    logger.fine(message) ;

        同时, 还可以使用 log 方法指定级别, 例如:

    logger.log(Level.FINE, message);

        提示:认的日志配置记录了 INFO 或更高级别的所有记录, 因此,应该使用 CONFIG、 FINE, FINER 和 FINEST 级别来记录那些有助于诊断,但对于程序员又没有太大意义的 调试信息。

        警告:如果将记录级别设计为 INFO 或者更低, 则需要修改日志处理器的配置。 默认的 日志处理器不会处理低于 INFO 级别的信息。 更加详细的内容请参看下一节。

        默认的日志记录将显示包含日志调用的类名方法名, 如同堆栈所显示的那样。但是, 如果虚拟机对执行过程进行了优化,就得不到准确的调用信息。此时,可以调用 logp 方法获 得调用类和方法的确切位置, 这个方法的签名为:

    void logp(Level 1, String className, String methodName, String message)

        下面有一些用来跟踪执行流的方法:

    void entering(String dassName , String methodName)
    void enteringCString className , String methodName , Object param)
    void entering(String className , String methodName , Object[] params)
    void exiting(String className , String methodName)
    void exiting(String className , String methodName , Object result)

        例如:

    int read(String file, String pattern)
    {
      1ogger.entering("com.mycompany.mylib.Reader", "read",
        new Object[] { file, pattern });
      ...   1ogger.exiting(
    "com.mycompany.mylib. Reader", "read", count);   return count ; }

        这些调用将生成 FINER 级别和以字符串 ENTRY 和 RETURN 开始的日志记录。

        注释: 在未来, 带 Object[] 参数的曰志记录方法可能会被重写, 以便支持变量参数列表 “( varargs”)。此后就可以用 logger.entering (“ com.mycompany.mylib.Reader”,“ read”, file, pattern) 格式调用这个方法了。

        记录日志的常见用途是记录那些不可预料的异常。可以使用下面两个方法提供日志记录 中包含的异常描述内容。

    void throwing(String className , String methodName , Throwable t)
    void log(Level 1 , String message , Throwable t)

        典型的用法是:

    if (...)
    {
      IOException exception = new IOException(". . .");
      1ogger.throwing("com.mycompany.mylib.Reader", "read", exception) ;
      throw exception;
    }

        还有

    try
    {
      ... }
    catch (IOException e) {   Logger.getLogger("com.mycompany.myapp").log(Level .WARNING , "Reading image", e); }

        调用 throwing 可以记录一条 FINER 级别的记录和一条以 THROW 开始的信息。

      7.5.3 修改日志管理器配置

        可以通过编辑配置文件来修改日志系统的各种属性。在默认情况下,配置文件存在于:

    jre/lib/1ogging.properties

        要想使用另一个配置文件, 就要将 java.utiil.logging.config.file 特性设置为配置文件的存储位置, 并用下列命令启动应用程序:

    java -Djava.util.logging.config. file=configFile MainClass

        警告: 日志管理器在 VM 启动过程中初始化, 这在 main 执行之前完成。 如果在 main 中调用 System.setProperty("java.util. logging.config. file",file), 也 会调 用 LogManager. readConfiguration() 来重新初始化曰志管理器

        要想修改默认的日志记录级别, 就需要编辑配置文件,并修改以下命令行

    .level=INFO

        可以通过添加以下内容来指定自己的日志记录级别

    com.mycompany.myapp.level=FINE

        也就是说,在日志记录器名后面添加后缀 .level。

        在稍后可以看到,日志记录并不将消息发送到控制台上,这是处理器的任务。另外,处理器也有级别。要想在控制台上看到 FINE 级别的消息, 就需要进行下列设置

    java.util.logging.ConsoleHandler.level=FINE

        警告: 在曰志管理器配置的属性设置不是系统属性, 因此, 用 -Dcom.mycompany.myapp. level= FINE 启动应用程序不会对日志记录器产生任何影响 。

        警告: 截止到 Java SE 7, Logmanager 类的 API 文档主张通过 Preferences API 设置 java.utill. logging.config.class 和 java.util.logging.config.file 属性。这是不正确的, 有关信息请参看 Javabug 数据库中的第 4691587 号 bug ( http://bugs.sun.com/bugdatabase )

        注释:日志属性文件由 java.util.logging.LogManager 类处理。可以通过将 java.util.logging. manager 系统属性设置为某个子类的名字来指定一个不同的日志管理器。 另外, 在保存标准日志管理器的同时, 还可以从日志属性文件跳过初始化。还有一种方式是将 java, util.logging.config.class 系统属性设置为某个类名,该类再通过其他方式设定日志管理器 属性。 有关 LogManager 类的佯细内容请参看 API 文档。

        在运行的程序中, 使用 jconsole 程序也可以改变日志记录的级别。 有关信息请参看 www.oracle.com/technetwork/articles/java/jconsole-1564139.html#LoggingControl 。

      7.5.4 本地化

        我们可能希望将日志消息本地化, 以便让全球的用户都可以阅读它。 应用程序的国际化 问题将在卷 II 的第 5 章中讨论。下面简要地说明一下在本地化日志消息时需要牢记的一些 要点。

        本地化的应用程序包含资源包( resource bundle ) 中的本地特定信息。资源包由各个地区 ( 如美国或德国)的映射集合组成。 例如, 某个资源包可能将字符串“ readingFile” 映射成英 文的 “ Reading file” 或者德文的“ Achtung! Datei wird eingelesen”。

        一个程序可以包含多个资源包, 一个用于菜单;其他用于日志消息。每个资源包都有一个名字(如 com.mycompany.logmessages)。要想将映射添加到一个资源包中,需要为每个地 区创建一个文件。英文消息映射位于 com/mycompany/logmessages_en.properties 文件中; 德 文消息映射位于 com/mycompany/logmessages_de.properties 文件中。(en 和 de 是语言编码)。可以将这些文件与应用程序的类文件放在一起, 以便 ResourceBundle 类自动地对它们进行定 位。这些文件都是纯文本文件, 其组成如下所示:

    readingFile=Achtung! Datei wird eingelesen
    renamingFile=Datei wird umbenannt
    ...

        在请求日志记录器时,可以指定一 个资源包:

    Logger logger = Logger.getLogger(1oggerName , "com.mycompany.logmessages");

        然后,为日志消息指定资源包的关键字,而不是实际的日志消息字符串。

    logger.info("readingFi1e");

        通常需要在本地化的消息中增加一些参数, 因此, 消息应该包括占位符 {0}、 {1} 等。例 如, 要想在日志消息中包含文件名,就应该用下列方式包括占位符:

    Reading file {0}.
    Achtung! Datei {0} wird eingelesen.

        然后,通过调用下面的一个方法向占位符传递具体的值:

    logger.log(Level .INFO, "readingFile", fileName);
    logger,log(Level .INFO, "renamingFile", new Object[] { oldName , newName });

      7.5.5 处理器

        在默认情况下, 日志记录器将记录发送到 ConsoleHandler 中, 并由它输出到 System.err 流中。特别是,日志记录器还会将记录发送到父处理器中,而最终的处理器(命名为“ ” )有 一个 ConsoleHandler。

        与日志记录器一样,处理器也有日志记录级别。对于一个要被记录的日志记录,它的日志记录级别必须高于日志记录器和处理器的阈值。日志管理器配置文件设置的默认控制台处 理器的日志记录级别为

    java.util.1ogging.ConsoleHandler.level =INF0

        要想记录 FINE 级别的日志,就必须修改配置文件中的默认日志记录级别和处理器级别。 另外,还可以绕过配置文件,安装自己的处理器。

    Logger logger = Logger.getLogger("com.mycompany.myapp");
    logger.setLevel (Level .FINE);
    1ogger.setUseParentHandlers(false);
    Handler handler = new ConsoleHandler();
    handler.setLevel (Level .FINE);
    1ogger.addHandler(handler):

        在默认情况下, 日志记录器将记录发送到自己的处理器父处理器。我们的日志记录器是原始日志记录器(命名为“ ”)的子类, 而原始日志记录器将会把所有等于或高于 INFO级別的记录发送到控制台。然而, 我们并不想两次看到这些记录。 鉴于这个原因,应该将 useParentHandlers 属性设置为 false。

        要想将日志记录发送到其他地方, 就要添加其他的处理器。日志 API为此提供了两个很有用的处理器, 一个是 FileHandler ; 另 一个是 SocketHandler。SocketHandler 将记录发送到特定的主机和端口。 而更令人感兴趣的是 FileHandler, 它可以收集文件中的记录

        可以像下面这样直接将记录发送到默认文件的处理器:

    FileHandler handler = new FileHandler();
    1ogger.addHandler(handler);

        这些记录被发送到用户主目录的 javan.log 文件中, n 是文件名的唯一编号。 如果用户系 统没有主目录( 例如, 在 Windows95/98/Me,) 文件就存储在 C:Window 这样的默认位置上。 在默认情况下, 记录被格式化为 XML。下面是一个典型的日志记录的形式:

    <record>
        <date>2002-02-04T07:45:15< /date>
        <millis>1012837515710</nillis>
        <sequence>l</sequence>
        <1ogger>com.mycompany.myapp</logger>
        <level>INF0</1evel>
        <class>com.mycompany.mylib.Reader</class>
        <method> read</method>
        <thread>10< /thread>
        <message>Reading file corejava.gif</message>
    </record>

        可以通过设置日志管理器配置文件中的不同参数(请参看表 7-1 ,) 或者利用其他的构造器 (请参看本节后面给出的 API 注释)来修改文件处理器的默认行为。

        也有可能不想使用默认的日志记录文件名, 因此, 应该使用另一种模式, 例如, %h/ myapp.log ( 有关模式变量的解释请参看表 7-2 )。

        如果多个应用程序 (或者同一个应用程序的多个副本)使用同一个日志文件, 就应该开 启 append 标志。 另外, 应该在文件名模式中使用 %u, 以便每个应用程序创建日志的唯一 副本。

        开启文件循环功能也是一个不错的主意。日志文件以 myapp.log.0, myapp.log.1 , myapp. log.2, 这种循环序列的形式出现。只要文件超出了大小限制, 最旧的文件就会被删除, 其他 的文件将重新命名, 同时创建一个新文件, 其编号为 0。

        提示: 很多程序员将曰志记录作为辅助文档提供给技术支持员工。如果程序的行为有误, 用户就可以返回查看日志文件以找到错误的原因 , 在这种情况下, 应该开启“ append” 标志, 或使用循环日志, 也可以两个功能同时使用。

        还可以通过扩展 Handler 类或 StreamHandler 类自定义处理器。在本节结尾的示例程序中 就定义了这样一个处理器。 这个处理器将在窗口中显示日志记录(如图 7-2 所示)。

         这个处理器扩展于 StreamHandler 类, 并安装了一个流。这个流的 write 方法将流显示输 出到文本框中。

    class WindowHandler extends StreamHandler
    {
        public WindowHandler()
        {
            ...
            final JTextArea output = new JTextArea();
            setOutputStream(new
                OutputStream()
                {
                public void write(int b) {} // not called
                public void write(byte[] b, int off, int len)
                {
                    output.append(new String(b, off, len));
                }
            });
        }
        ...
    }

        使用这种方式只存在一个问题, 这就是处理器会缓存记录, 并且只有在缓存满的吋候才将 它们写入流中, 因此, 需要覆盖 publish 方法, 以便在处理器获得每个记录之后刷新缓冲区。

    class WindowHandler extends StreamHandler
    {
        ...
        public void publish(LogRecord record)
        {
            super.publish(record) ;
            flush();
        }
    }

        如果希望编写更加复杂的流处理器,就应该扩展 Handler 类, 并自定义 publish、 flush 和 close 方法。

      7.5.6 过滤器

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

    boolean isLoggab1e(LogRecord record)

        在这个方法中,可以利用自己喜欢的标准,对日志记录进行分析,返回 true 表示这些记 录应该包含在日志中。例如,某个过滤器可能只对 entering 方法和 exiting 方法产生的消息 感兴趣,这个过滤器可以调用 record.getMessage() 方法,并査看这个消息是否用 ENTRY 或 RETURN 开头。

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

      7.5.7 格式化器

        ConsoleHandler 类和 FileHandler 类可以生成文本和 XML 格式的日志记录。但是, 也可以自定义格式。这需要扩展 Formatter 类并覆盖下面这个方法:

    String format(LogRecord record)

        可以根据自己的愿望对记录中的信息进行格式化,并返冋结果字符串。在 format 方法 中, 有可能会调用下面这个方法

    String formatMessage(LogRecord record)

        这个方法对记录中的部分消息进行格式化、 参数替换和本地化应用操作。

        很多文件格式(如 XML) 需要在已格式化的记录的前后加上一个头部和尾部。在这个例 子中,要覆盖下面两个方法:

    String getHead (Handler h)
    String getTail (Handler h)

        最后,调用 setFormatter 方法将格式化器安装到处理器中

      7.5.8 日志记录说明

        面对日志记录如此之多的可选项, 很容易让人忘记最基本的东西。下面的“ 日志说明书”总结了一些最常用的操作。

        1 ) 为一个简单的应用程序, 选择一个日志记录器,并把日志记录器命名为与主应用程序包一样的名字,例如,com.mycompany.myprog, 这是一种好的编程习惯。 另外,可以通过 调用下列方法得到日志记录器。

    Logger logger = Logger.getLogger("com.mycompany.myprog");

        为了方便起见,可能希望利用一些日志操作将下面的静态域添加到类中:

    private static final Logger logger = Logger.getLogger("com.mycompany.myprog"):

        2 ) 默认的日志配置将级别等于或高于 INFO 级别的所有消息记录到控制台。用户可以覆盖默认的配置文件。但是正如前面所述,改变配置需要做相当多的工作。因此,最好在应用程序中安装一个更加适宜的默认配置

        下列代码确保将所有的消息记录到应用程序特定的文件中。可以将这段代码放置在应用 程序的 main方法中。

    if (System,getProperty("java,util.logging.config.class") == null
        && System.getProperty("java.util.logging.config.file") == null)
    {
        try
        {
            Logger.getLogger("").setLevel(Level.ALL);
            final int LOC_ROTATION_C0UNT = 10;
            Handler handler = new FileHandler('%h/myapp.log", 0, LOG_ROTATI0N_COUNT):
            Logger.getLogger("").addHandler(handler):
        }
        catch (IOException e)
        {
            logger.log(Level.SEVERE, "Can't create log file handler", e);
        }
    }

        3 ) 现在,可以记录自己想要的内容了。但需要牢记:所有级别为 INFO、 WARNING 和 SEVERE 的消息都将显示到控制台上。 因此, 最好只将对程序用户有意义的消息设置为这几 个级别。将程序员想要的日志记录,设定为 FINE 是一个很好的选择

        当调用 System.out.println 时, 实际上生成了下面的日志消息:

    logger.fine("File open dialog canceled");

        记录那些不可预料的异常也是一个不错的想法,例如:

    try
    {
        ...
    }
    catch (SomeException e)
    {
        logger.log(Level.FINE, "explanation", e);
    }

        程序清单 7-2 利用上述说明可实现:日志记录消息也显示在日志窗口中。(略过)

        API Java.util.logging.Logger 14

        • Logger getLogger(String loggerName)

        • Logger getLogger(String loggerName, String bundleName)

          获得给定名字的日志记录器。如果这个日志记录器不存在, 创建一个日志记录器。

          参数 loggerName 具有层次结构的日志记录器名。例如,com.mycompany.myapp bundleName 用来查看本地消息的资源包名

        • void severe(String message)

        • void warning(String message)

        • void info(String message)

        • void config(String message)

        • void fine(String message)

        • void finer(String message)

        • void finest(String message)

          记录一个由方法名和给定消息指示级别的日志记录。

        • 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)

          记录一个描述进入 /退出方法的日志记录, 其中应该包括给定参数和返回值。

        • void throwing(String className, String methodName, Throwable t)

          记录一个描述拋出给定异常对象的日志记录。

        • void log(Level level, String message)

        • void log(Level level, String message, Object obj)

        • void log(Level level, String message, Object[] objs)

        • void log(Level level, String message, Throwable t)

          记录一个给定级别和消息的日志记录, 其中可以包括对象或者可抛出对象。要想包括 对象, 消息中必须包含格式化占位符 {0}、 {1} 等。

        • void logp(Level level, String className, String methodName, String message)

        • void logp(Level level, String className, String methodName, String message, Object obj)

        • void logp(Level level, String className, String methodName, String message, Object[] objs)

        • void logp(Level level, String className, message, Throwable t)

          记录一个给定级别、 准确的调用者信息和消息的日志记录, 其中可以包括对象或可抛 出对象。 

        • void logrb(Level level,String className,String methodName,String bundleName,String message)

        • void logrb(Level level,String className,String methodName,String bundleName,String message,Object obj)

        • void logrb(Level level,String className,String methodName,String bundleName,String message,Object[] objs)

        • void logrb(Level level,String className,String methodName,String bundleName,String message,Throwable t)

          记录一个给定级别、 准确的调用者信息、 资源包名和消息的日志记录, 其中可以包括 对象或可拋出对象。

        • Level getLevel()

        • void setLevel(Level l)

          获得和设置这个日志记录器的级别。

        • Logger getPatent()

        • void setPatent(Logger l)

          获得和设置这个日志记录器的父日志记录器。

        • Handler[] gerHandlers()

          获得这个日志记录器的所有处理器。

        • void addHandler(Handler h)

        • void removeHandler(Handler h)

          增加或删除这个日志记录器中的一个处理器。

        • boolean getUserParentHandlers()

        • void setUserParentHandlers(boolean b)

          获得和设置“ use parent handler ” 属性。如果这个属性是 true, 则日志记录器会将全部的日志记录转发给它的父处理器。

        • Filter getFilter()

        • void setFilter(Filter f)

          获得和设置这个日志记录器的过滤器。

        API java.util.logging.Handler 1.4

        • abstract void publish(LogRecordrecord)

          将日志记录发送到希望的目的地。

        • abstract void flush()

          刷新所有已缓冲的数据

        • abstract void close( )

          刷新所有已缓冲的数据, 并释放所有相关的资源。

        • Filter getFilter( )

        • void setFilter(Filter f )

          获得和设置这个处理器的过滤器。

        • Formatter getFormatter( )

        • void setFormatter(Formatter f)

          获得和设置这个处理器的格式化器。

        • Level getLevel ( )

        • void setLevel (Level l )

          获得和设置这个处理器的级别。

        API java.util.logging.ConsoleHandler 1.4

        • ConsoleHandler( ) 构造一个新的控制台处理器。

        API java.util.logging.FileHandler 1.4

        • FileHandler(String pattern)

        • FileHandler(String pattern, boolean append )

        • FileHandler(String pattern, int limit, int count )

        • FileHandler(String pattern, int limit, int count, boolean append )

          构造一个文件处理器。

          参数: pattern 构造日志文件名的模式。参见表 7-2 列出的模式变量

              limit 在打开一个新日志文件之前, 日志文件可以包含的近似最大字节数

              count 循环序列的文件数量

              append 新构造的文件处理器对象应该追加在一个已存在的日志文件尾部, 则为 true

        API java.util.logging.LogRecord 1.4

        • Level getLevel( )

          获得这个日志记录的记录级别。

        • String getLoggerName( )

          获得正在记录这个日志记录的日志记录器的名字。

        • ResourceBundle getresourceBundle( )

        • String getresourceBundleName( )

          获得用于本地化消息的资源包或资源包的名字。如果没有获得,则返回 null。

        • String getMessage( )

          获得本地化和格式化之前的原始消息。

        • Object[] getParameters()

          获得参数对象。如果没有获得, 则返回 null。

        • Throwable getThrown()

          获得被拋出的对象。 如果不存在, 则返回 null。

        • String getSourceClassName()

        • String getSourceMethodName()

          获得记录这个日志记录的代码区域。这个信息有可能是由日志记录代码提供的, 也有可能是自动从运行时堆栈推测出来的。如果日志记录代码提供的值有误,或者运行时 代码由于被优化而无法推测出确切的位置,这两个方法的返回值就有可能不准确。

        • long getMillis()

          获得创建时间。以毫秒为单位(从 1970 年开始 。)

        • long getSequenceNumber()

          获得这个日志记录的唯一序列序号。

        • int getThreadID()

          获得创建这个日志记录的线程的唯一 ID。 这些 ID 是由 LogRecord 类分配的,并且与 其他线程的 ID 无关。

        API java.util.logging.Filter 1.4

        • boolean isLoggable(LogRecord record)

          如果给定日志记录需要记录, 则返回 true。

        API java.util.logging.Formatter 1.4

        • abstract String format(LogRecord record)

          返回对日志记录格式化后得到的字符串。

        • String getHead(Handler h)

        • String getTail(Handler h)

          返回应该出现在包含日志记录的文档的开头和结尾的字符串。超类 Formatter 定义了 这些方法,它们只返回空字符串。如果必要的话,可以对它们进行覆盖。

        • String formatMessage(LogRecord record)

          返回经过本地化和格式化后的日志记录的消息内容。

    7.6 调试技巧

      假设编写了一个程序, 并对所有的异常进行了捕获和恰当的处理,然后,运行这个程序,但 还是出现问题,现在该怎么办呢(如果从来没有遇到过这种情况, 可以跳过本章的剩余部分)?

      当然,如果有一个方便且功能强大的调试器就太好了。调试器是 Eclipse、 NetBeans 这类专业集成开发环境的一部分。在启动调试器之前,本节先给出一些有价值的建议。

      1 ) 可以用下面的方法打印或记录任意变量的值:

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

      如果 x 是一个数值,则会被转换成等价的字符串。如果 x 是一个对象,那么 Java 就会调 用这个对象的 toString方法。要想获得隐式参数对象的状态,就可以打印 this 对象的状态。

    Logger.getClobal().info("this=" + this);

      Java 类库中的绝大多数类都覆盖了 toString 方法, 以便能够提供有用的类信息。这样会 使调试更加便捷。在自定义的类中,也应该这样做。

      2 ) 一个不太为人所知但却非常有效的技巧是在每一个类中放置一个单独的 main方法。 这样就可以对每一个类进行单元测试。

    public class MyClass
    {
        methods and fields
        ...
        public static void main(String[] args)
        {
            test code
        }
    }

      利用这种技巧, 只需要创建少量的对象, 调用所有的方法, 并检测每个方法是否能够 正确地运行就可以了。另外, 可以为每个类保留一个 main方法,然后分别为每个文件调用 Java 虚拟机进行运行测试。在运行 applet 应用程序的时候, 这些 main方法不会被调用,而 在运行应用程序的时候,Java 虚拟机只调用启动类的 main方法

      3 ) 如果喜欢使用前面所讲述的技巧,就应该到 http://junit.org 网站上査看一下 JUnit。 JUiiit 是一个非常常见的单元测试框架,利用它可以很容易地组织测试用例套件。只要修改类,就需要运行测试。在发现 bug 时,还要补充一些其他的测试用例。

      4 ) 日志代理( logging proxy) 是一个子类的对象, 它可以截获方法调用, 并进行日志记 录,然后调用超类中的方法。例如,如果在调用 Random 类的 nextDouble 方法时出现了问 题, 就可以按照下面的方式,以匿名子类实例的形式创建一个代理对象:

    Random generator = new Random()
    {
        public double nextDouble()
        {
            double result = super.nextDouble();
            Logger.getClobal().info("nextDouble: " + result);
            return result;
        }
    };

      当调用 nextDouble 方法时, 就会产生一个日志消息。要想知道谁调用了这个方法, 就要生成 一个堆栈轨迹

      5 ) 利 用 Throwable 类提供的 printStackTrace 方法,可以从任何一个异常对象中获得堆栈情况。下面的代码将捕获任何异常,打印异常对象和堆栈轨迹, 然后,重新拋出异常, 以便能够找到相应的处理器。

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

      不一定要通过捕获异常来生成堆栈轨迹。只要在代码的任何位置插入下面这条语句就可 以获得堆栈轨迹

    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 > errors.txt

      而是采用下面的方式捕获错误流:

    java MyProgram 2> errors.txt

      要想在同一个文件中同时捕获 System.err和 System.out, 需要使用下面这条命令

    java MyProgram 1> errors.txt 2>&1

      这条命令将工作在 bash 和 Windows shell 中。

      8 ) 让非捕获异常的堆栈轨迹出现在 System.err 中并不是一个很理想的方法。如果在客户 端偶然看到这些消息,则会感到迷惑,并且在需要的时候也无法实现诊断目的。比较好的方式是将这些内容记录到一个文件中。可以调用静态的 Thread.setDefaultUncaughtExceptionHandler 方法改变非捕获异常的处理器:

    Thread.setDefaultUncaughtExceptionHandlerf
        new Thread.UncaughtExceptionHandler()
        {
            public void uncaughtException(Thread t, Throwable e)
            {
    
                save information in logfile
            };
        });

      9 ) 要想观察类的加载过程, 可以用 -verbose 标志启动 Java 虚拟机。这样就可以看到如 下所示的输出结果:

    [Opened /usr/local /jdk5•0/jre/1i b/rt• ar]
    [Opened /usr/local /jdk5.0/jre/lib/jsse.jar]
    [Opened /usr/local /jdk5.0/jre/lib/jce.jar]
    [Opened /usr/local /jdk5.0/j re/1i b/charsets.jar]
    [Loaded java.lang.Object from shared objects file]
    [Loaded java.io.Serializable from shared objects file]
    [Loaded java.lang.Comparable from shared objects file]
    [Loaded java.lang.Cha「Sequence from shared objects file]
    [Loaded java.lang.String from shared objects file]
    [Loaded java.1ang.reflect.Gene ricDecl arati on from shared objects file]
    [Loaded java.lang.reflect.Type from shared objects file]
    [Loaded java.lang.reflect.AnnotatedElement from shared objects file]
    [Loaded java.lang.Cl ass from shared objects file]
    [Loaded java.lang.Cloneable from shared objects file]
    ...

      有时候, 这种方法有助于诊断由于类路径引发的问题。

      10 ) -Xlint 选项告诉编译器对一些普遍容易出现的代码问题进行检査。例如, 如果使用 下面这条命令编译:

    javac -Xlint:fall through

      当 switch 语句中缺少 break 语句时, 编译器就会给出报告(术语“ lint” 最初用来描述一种定 位 C 程序中潜在问题的工具,现在通常用于描述查找可疑但不违背语法规则的代码问题的工具。)

      下面列出了可以使用的选项:

      11 ) java 虚拟机增加了对 Java 应用程序进行监控(monitoring) 和管理 (management) 的 支持。它允许利用虚拟机中的代理装置跟踪内存消耗、 线程使用、 类加载等情况。这个功能对于像应用程序服务器这样大型的、 长时间运行的 Java 程序来说特别重要。下面是一个能够 展示这种功能的例子:JDK 加载了一个称为jconsole 的图形工具,可以用于显示虚拟机性能 的统计结果, 如图 7-3 所示。找出运行虚拟机的操作系统进程的 ID。在 UNIX/Linux 环境下, 运行 ps 实用工具, 在 Windows 环境下,使用任务管理器。然后运行 jconsole 程序:

    jconsole processID

      控制台给出了有关运行程序的大量信息。有关更加详细的信息参见 www.orade.com/ technetwork/articles/java/jconsole-1564139.html。

      12 ) 可以使用 jmap 实用工具获得一个堆的转储, 其中显示了堆中的每个对象。使用命 令如下:

    jmap -dump:format=b, file=dumpFileName processID
    jhat dumpFileName

      然后,通过浏览器进入localhost:7000, 将会运行一个网络应用程序,借此探查转储对象时堆的内容。

      13 ) 如果使用 -Xprof 标志运行 Java 虚拟机, 就会运行一个基本的剖析器来跟踪那些代 码中经常被调用的方法。剖析信息将发送给 System.out。输出结果中还会显示哪些方法是由 即时编译器编译的。

      警告: 编译器的 -X 选项并没有正式支持, 而且在有些 JDK 版本中并不存在这个选项。 可以运行命令 java -X 得到所有非标准选项的列表。

      本章介绍了异常处理和日志,另外还讲解了关于测试和调试的一些有用的技巧。接下来 两章会介绍泛型程序设计和它最重要的应用:Java 集合框架。

    恭喜,本章完!

  • 相关阅读:
    关于在Linux下的换行符 和windows下的换行符
    Linux文件操作标准接口
    tcpdump抓包和wireshark解包
    Makefile学习(1)
    域名服务器设置
    Linux系统移植(1)
    SQL基本语句整理
    ARM---搭建开发板的开发环境(x210v3s)
    C语言基础
    Oracle中的USEREVN()
  • 原文地址:https://www.cnblogs.com/yangjingkang/p/14426787.html
Copyright © 2011-2022 走看看