首页 > 其他 > 详细

第7章 异常、断言和日志

时间:2020-03-17 22:28:01      阅读:62      评论:0      收藏:0      [点我收藏+]

目录

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

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

  • 向用户报告错误;
  • 保存所有的工作结果;
  • 允许用户以妥善的形式退出程序。

对于异常情况(可能造成程序崩溃的错误输入),Java 使用一种称为 异常处理exception handing)的错误捕获机制处理。

7.1 处理错误 264

假设一个 Java 程序运行期间出现了一个错误,错误可能是:

  1. 文件包含了错误信息
  2. 网络连接出现问题
  3. 使用无效的数组下标
  4. 试图使用一个没有被赋值的对象引用

由于出现错误而使得某些操作没有完成,程序应该:

  • 返回到一种安全状态,并能够让用户执行一些其他的命令;
  • 允许用户保存所有操作的结果,并以妥善的方式终止程序。

这并不是一件很容易的事情,检测错误条件的代码 通常离 能够让数据恢复到安全状态,或者能够保存用户的操作结果,并正常地退出程序的代码 很远。
异常处理的任务就是将控制权从错误产生的地方转移给能够处理这种情况的错误处理器。

为了能够在程序中处理异常情况,必须研究程序中可能会出现的错误和问题,以及哪类问题需要关注。

  1. 用户输入错误
  2. 设备错误
  3. 物理限制,硬盘存储空间用完
  4. 代码错误(方法返回错误答案、错误调用其他方法、计算的数组索引不合法、试图在散列表中查找一个不存在的记录、试图让一个空栈执行弹出操作)

传统的做法:
方法返回一个特殊的错误码,由调用方法分析。
返回值 -1 或者 null 引用。

遗憾的是,并不是在任何情况下都能够返回一个错误码。
有可能无法明确地将有效数据和无效数据加以区分。

异常处理机制:

  • 程序方法运行过程中发生错误;
  • 如果抛出异常(一个封装了错误信息的对象),控制权转移;
  • 方法立刻退出,并不返回任何值;
  • 开始搜索能够能够处理异常状况的异常处理器(exception handler),异常处理器对异常进行处理。
  • 如果没有处理器捕获这个异常,程序终止。

7.1.1 异常分类 265

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

Throwable

  • Error
  • Exception

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

Exception 层次结构:

  • RuntimeException(程序错误导致的异常)
  • 其他异常(程序本身没有问题,类似 I/O 错误这类问题导致的异常 -> 外部环境)

RuntimeException

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

其他异常:

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

如果出现 RuntimeException 异常,那么就一定是你的问题。

应该通过检测数组下标是否越界来避免 ArrayIndexOutOfBoundException 异常;
应该通过在使用变量之前检测是否为 null 来杜绝 NullPointerException 异常。

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

7.1.2 声明受查异常 267

如果遇到了无法处理的情况,那么 Java 方法可以抛出一个异常。
一个方法不仅需要告诉编译器将要返回什么值,还要告诉编译器有可能发生什么错误

方法应该在其首部声明所有可能抛出的异常。

public FileInputStream(String name) throws FileNotFoundException

构造一个 FileInputStream 对象,或者 抛出一个 FileNotFoundException 异常

如果方法抛出一个异常,运行时系统就会开始搜索异常处理器,以便知道如何处理异常。

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

  1. 调用一个抛出受查异常的方法(FileInputStream 构造器)。
  2. 程序运行过程中发现错误,并且利用 throw 语句抛出一个受查异常。
  3. 程序出现错误(ArrayIndexOutOfBoundsExceptionRuntimeException)。
  4. Java 虚拟机和运行时库出现的内部错误(Error)。

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

不要声明从 Error 继承的错误。任何程序代码都具有抛出那些异常的潜能,而我们对其没有任何控制能力。
不应该声明从 RuntimeException 继承的非受查异常。
运行时错误完全在我们的控制之下。应该将更多的时间花费在修正程序中的错误上,而不是说明这些错误发生的可能性上。

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

除了声明异常,还可以捕获异常(异常不被抛到方法之外,也不需要 throws 规范)。

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

7.1.3 如何抛出异常 269

IOException 异常
EOFxception 异常:在输入过程中,遇到了一个未预期的 EOF 后的信号。

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

对于一个已经存在的异常类,将其抛出非常容易。

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

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

7.1.4 创建异常类 270

在程序中,可能会遇到任何标准异常类都没有能够充分地描述清楚的问题。

如何创建自己的异常类?
定义一个派生于 Exception 的类,或者派生于 Exception 子类的类。

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

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

抛出自己定义的异常类型:

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

7.2 捕获异常 271

有些代码必须捕获异常,捕获异常需要进行周密的计划。

7.2.1 捕获异常 271

如果某个异常发生的时候没有在任何地方进行捕获,程序就会终止执行,并在控制台上打印异常信息,其中包括异常的类型和堆栈的内容。

捕获一个异常,必须设置 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 子句中没有声明的异常类型,那么这个方法就会立刻退出(漏网之鱼)。

演示捕获异常的处理过程,读取数据的典型程序代码:

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

除了捕获异常,还有其他的选择吗?

通常,最好的选择是什么也不做,而是将异常传递给调用者(早抛出,晚捕获)。

声明方法可能会抛出一个 IOException

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

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

哪种方法更好呢?

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

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

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

这个规则也有一个例外:如果编写一个覆盖超类的方法,而这个方法又没有抛出异常,那么这个方法就必须捕获方法代码中出现的每一个受查异常(只能捕获异常,不能传递异常)。

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

7.2.2 捕获多个异常 273

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 action for all other I/O problems
}

e.getMessage() 详细的错误信息
e.getClass().getName() 异常对象的实际类型

同一个 catch 子句可以捕获多个异常类型。
合并 catch 子句:

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

只有当捕获的异常类型彼此之间不存在子类关系时才可以使用。

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

7.2.3 再次抛出异常与异常链 274

catch 子句中可以抛出一个异常,目的是改变异常的类型。

例如,执行 servlet 的代码可能不想知道发生错误的细节原因,但希望明确地知道 servlet 是否有问题。

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

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

一种更好的处理办法,将原始异常设置为新异常的“原因”:

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

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

Thorowable e = se.getCause();

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

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

7.2.4 finally子句 275

当代码抛出一个异常时,就会终止方法中剩余代码的处理,并退出这个方法的执行。
如果方法获得了一些本地资源,并且只有这个方法自己知道,又如果这些资源在退出方法之前必须被回收,那么就会产生资源回收问题。

一种解决方案是捕获并重新抛出所有的异常。
这种解决方案比较乏味,因为需要在两个地方清除所分配的资源。一个在正常的代码中;另一个在异常代码中。

Java 有一种更好的解决方案,finally 子句。

Java 中如何恰当地关闭一个文件?
如果使用 Java 编写数据库程序,就需要使用同样的技术关闭与数据库的连接。
发生异常时,恰当地关闭所有数据库的连接是非常重要的。

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


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

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

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

强烈建议解耦合 try/catchtry/finally 语句块。提高代码清晰度。

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

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

有时候,finally 子句也会带来麻烦。
例如,清理资源的方法也有可能抛出异常。

InputStream 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) {
        ex = e;
        throw e;
    }
} finally {
    try {
        in.close();
    } catch (Exception e) {
        if (ex == null) throw e;
    }
}

7.2.5 带资源的try语句 278

对于以下代码模式:

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

假设资源属于一个实现了 AutoCloseable 接口的类,AutoCloseable 接口有一个方法:

void close() throws Exception

带资源的 try 语句的最简形式为:

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() 方法。

还可以指定多个资源:

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

inout 自动关闭。

如果用常规方式手动编程,就需要两个嵌套的 try/finally 语句。

如果 try 块抛出一个异常,而且 close 方法也抛出一个异常,那么原来的异常就会丢失,转而抛出 close 方法的异常。
带资源的 try 语句可以很好的解决这种情况。

  • 原来的异常会重新抛出
  • close 方法抛出的异常会“被抑制”
  • 这些异常将自动捕获,并由 addSuppressed 方法增加到原来的异常
  • 如果对这些异常感兴趣,可以调用 getSuppressed 方法,它会得到从 close 方法抛出并被抑制的异常列表。

采用常规方式编程,代码极其繁琐。
关闭资源,尽可能使用带资源的 try 语句。

7.2.6 分析堆栈轨迹元素 280

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

Throwable 类:

  • printStackTrace 方法 访问堆栈轨迹的文本描述信息
  • getStackTrace 方法 得到 StackTraceElement 对象的一个数组。

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

7.3 使用异常机制的技巧 282

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

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

2. 不要过分细化异常

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

这种编程方式将导致代码量的急剧膨胀。
有必要将整个任务包装在一个 try 语句块中,这样,当任何一个操作出现问题时,整个任务都可以取消。

异常处理机制的其中一个目标:将正常处理与错误处理分开。

3. 利用异常层次结构

不要只抛出 RuntimeException 异常。应该寻找更加适当的子类或创建自己的异常类。
不要只捕获 Throwable 异常,否则,会使程序代码更难读、更难维护。
考虑受查异常与非受查异常的区别。已检查异常本来就很庞大,不要为逻辑错误抛出这些异常。

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

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. 不要羞于传递异常 -> 晚捕获

很多程序员认为应该捕获抛出的全部异常。

如果程序员调用了一个抛出异常的方法,例如,FileInputStream 构造器 或 readLine 方法,程序员就会本能地捕获这些可能产生的异常。
其实,传递异常比捕获这些异常更好:

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

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

7.4 使用断言 285

7.4.1 断言的概念 285

7.4.2 启用和禁用断言 286

7.4.3 使用断言完成参数检查 287

7.4.4 为文档假设使用断言 288

7.5 记录日志 289

7.5.1 基本日志 289

7.5.2 高级日志 289

7.5.3 修改日志管理器配置 291

7.5.4 本地化 292

7.5.5 处理器 293

7.5.6 过滤器 296

7.5.7 格式化器 296

7.5.8 日志记录说明 296

7.6 调试技巧 304

假设编写了一个程序,并对所有的异常进行了捕获和恰当的处理,然后运行这个程序,但还是出现问题,怎么办?

一个方便且功能强大的调试器

启动调试器之前,可以按照如下建议处理:

1. 可以用如下方法打印或记录任意变量的值

System.out.println("x=" + x);

Logger.getGlobal().info("x=" + x);

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

2. 一个不太为人所知但却非常有效的技巧是在每一类中放置一个单独的 main 方法

对每一个类进行单元测试。

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

利用这种技巧,只需要创建少量的对象,调用所有的方法,并检测每个方法是否能够正确地运行。

3. JUnit

JUnit 是一个常见的单元测试框架,利用它可以很容易地组织测试用例套件。

4. 日志代理(logging proxy

日志代理(logging proxy)是一个子类对象,它可以截获方法调用,并进行日志记录,然后调用超类中的方法。

5. 利用 Throwable 类提供的 printStackTrace 方法,可以从任何一个异常对象中获得堆栈情况

不一定要通过捕获异常来生成堆栈轨迹。

Thread.dumpStack()

6. 一般来说,堆栈轨迹显示在 System.err

可以利用 printStackTrace(PrinterWriter s) 方法将它发送到一个文件中。

7. 通常,将一个程序中的错误信息保存在一个文件中是非常有用的

然而,错误信息被发送到 System.err 中,而不是 System.out 中。

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

java MyProgram 2> errors.txt

同一个文件中同时捕获 System.errSystem.out

java MyProgram 1> errors.txt 2>&1

8. 让非捕获异常的堆栈轨迹出现在 System.err 中并不是一个很理想的方法

比较好的方式是将这些内容记录到一个文件中。

9. 要想观察类的加载过程,可以用 -verbose 标志启动 Java 虚拟机

有助于诊断由于类路径引发的问题

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

javac -Xlint:fallthrough

switch 语句中缺少 break 语句时,编译器就会给出报告。

术语 lint 最初用来描述一种定位 C 程序中潜在问题的工具,现在通常用于描述查找可疑但不违背语法规则的代码问题的工具。

11. Java 虚拟机增加了对 Java 应用程序进行监控(monitoring)和管理(management)的支持

它允许利用虚拟机中的代理装置跟踪内存消耗、线程使用、类加载等情况。

JDK 有一个称为 jconsole 的图形工具,可以用于显示虚拟机性能的统计结果。

jconsole processID

12. 可以使用 jmap 实用工具获得一个堆的转储

其中显示了堆中的每个对象

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

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

13. 如果使用 -Xprof 标志运行 Java 虚拟机,就会运行一个基本的剖析器来跟踪那些代码中经常被调用的方法

剖析信息将发送给 System.out
输出结果中还会显示哪些方法是由即时编译器编译的。

编译器的 -X 选项并没有正式支持,可以运行命令 java -X 得到所有非标准选项的列表。

第7章 异常、断言和日志

原文:https://www.cnblogs.com/clipboard/p/12513883.html

(0)
(0)
   
举报
评论 一句话评论(0
关于我们 - 联系我们 - 留言反馈 - 联系我们:wmxa8@hotmail.com
© 2014 bubuko.com 版权所有
打开技术之扣,分享程序人生!