Java核心技术卷一 5. java异常、断言和日志

处理错误

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

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

需要关注的问题:

  1. 用户输入错误
  2. 设备错误
  3. 物理限制
  4. 代码错误

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

异常分类

全都派生于 Throwable 类,分解为两个分支层次结构:Error 和 Exception

  • Error 类层次结构描述 Java 运行时系统的内部错误和资源耗尽错误。应用程序不该抛出这种错误。
  • Exception 类层次结构分为两个分支。
  • 派生于 RuntimeException 异常,由程序错误导致的异常。
  • 其他不是程序导致的异常。

派生于 RuntimeException 异常的几种情况:

  • 错误的类型转换
  • 数组访问越界,ArrayIndexOutOfBoundsException异常
  • 访问空指针,NullPointerException异常

其他异常的几种情况:

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

Java 将派生于 Error 类或 RuntieException 类额所有异常成为非受查异常,其他的异常成为受查异常

声明受查异常

一个方法不仅要告诉编译器要返回什么值,还要告诉编译器可能发生什么错误

例子:

public FileInputStream(String name) throws FileNotFoundException {
    ...
}

这个声明表示可能抛出异常,如果发生了这个异常构造器将不会初始化一个新的 FileInputStream 对象,而是抛出一个 FileNotFoundException 类对象。抛出异常类对象后,运行时系统就会搜索异常处理器,以便指定如何处理 FileNotFoundException 对象。

需要抛出异常的情况:

  1. 调用一个抛出受查异常的方法。
  2. 程序运行过程中发现错误,利用 throw 语句抛出一个受查异常。
  3. 程序出现错误,一些非受查对象。
  4. Java 虚拟机和运行时库出现的内部错误。

前两种情况,必须告诉调用这个方法的程序员有可能抛出异常。防止程序遇到异常停止线程。

根据异常规范在方法首部声明这个方法可能抛出的异常,多个受查异常用逗号隔开:

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

如果方法没有声明受查异常,编译器会发出一个错误信息。

除了声明异常外,还可以捕获异常,抛出让异常处理器处理。

另外:声明一个异常,可能抛出这个异常的子类异常。

抛出异常

抛出异常的语句:

throw new EOFException();

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

抛出以存在的异常类:

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

一旦方法抛出了异常,这个方法就不可能返回到调用者。

创建异常类

标准异常无法充分的描述清楚问题,可以创建自己的异常类:

class FileFormatException extends IOException {
    public FileFormatException(){}
    public FileFormatException(String gripe){
        super(gripe);//构造一个带描述信息的异常
    }
}

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

api:

//java.lang.Throwable
Throwable() 构造一个 Throwable 对象,没有描述信息。
Throwable(String message) 构造一个 Throwable 对象,带描述信息。
String getMessage() 获取描述信息。

# 捕获异常

捕获异常

异常发生,没有捕获,程序就会终止执行,在控制台打印异常信息。

捕获一行,必须设置 try/catch 语句块:

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

如果 try 内有代码抛出了 catch 中定义的异常:

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

注意:如果 try 内的异常 catch 中没有,则程序终止。

将异常传递给调用者:

public void read(String filename) throws IOException {...}

将不知道怎样处理的异常继续进行传递,传递异常使用 throws 说明符,告知调用者这个方法可能出现异常。

例外:如果编写一个覆盖超类的方法,这个方法没有抛出异常,那么这个方法必须捕获方法代码中出现的每一个受查异常。不允许在子类的 throws 说明符中出现超过超类方法所列出的异常类范围。

捕获多个异常

有两种方式,第一种方式为每个异常类使用单独的 catch 子句。

try {
    code;
} catch (FileNotFoundException e) {
    emergency action;
} catch (UnknownHostException e) {
    emergency action;
} catch (IOException e) {
    emergency action;
}

第二种方式可以对动机一样的异常进行合并 catch 子句。

try {
    code;
} catch (FileNotFoundException | UnknownHostException | IOException e) {
    emergency action;
}

获得对象的更多信息,可以:

e.getMessage();
e.getClass().getName();

注意:捕获多个异常时,异常变量隐含为 final 变量,不可修改。

再次抛出异常与异常链

有些异常我们并不想指定发送错误的细节原因,但希望明确的指定它是否有问题:

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

//重新得到原始异常
Throwable e = se.getCause();

这个受查异常,不允许抛出它,包装技术十分有用,可以捕获这个受查异常,把它包装成一个运行时异常。

finally 子句

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

InputStream in = new FileInputStream(...);
try {
    core 1;
    core 2;
} catch (IOException e) {
    core 3;
} finally {
    core 4;
}

会遇到3种情况:

  1. 代码没有异常。执行完 try 块,然后执行 finally 块。
  2. 抛出一个异常。执行 try 块知道异常为止,跳过剩余 try 代码,转去执行与异常匹配的 catch 子句中的代码,最后执行 finally 子句中的代码。
  3. 代码抛出异常,但是 catch 没有匹配的异常。执行 try 块知道异常为止,跳过剩余 try 代码,转去执行 finally 子句中的代码,并将异常抛给这个方法的调用者。

解耦合try/catchtry/finally,提高代码的清晰度:

try {
    try {
        code;
    } finally {
        in.close();
    }
} catch (IOException e) {
    show error message;
}

内层确保关闭输入流;外层确保报告出现的错误。外层也会报告 finally 子句中出现的错误。

return的各种场景

  1. return 在 try 块中时,方法返回前 finally 子句的内容将被执行。
  2. return 在 try 块 finally 子句中都存在时,finally 子句中的返回值将会覆盖原始的返回值。

finally子句的坏处

try {
    code 1;
} finally {
    in.close();
}

如果 try 语句抛出了一些非 IOEception 的异常,这些异常只有调用者才能处理。执行 finally 语句块,并调用 close 方法,有可能抛出 IOException 异常。有这种情况时,原始的异常将会丢失,转而抛出 close 方法的异常。

带资源的 try 语句

资源属于实现了 AutoCloseable 接口的类时:

//接口的一个方法
void close() throws Exception

//最简形式
try (Resource res = ...) {
    work with res;
}

try 块退出时,会自动调用 res.close() 。并且可以指定多个资源。

分析堆栈轨迹元素

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

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

使用异常机制的技巧

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

使用断言

断言的概念

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

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

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

例子,断言 x 是一个非负数值:

assert x >= 0;

assert x >= 0 : x;

启用和禁用断言

启用:java -enableassertions MyApp

启用某个类中的断言:java -ea:MyClass -ea:com.mycompany.mylib... MyApp

禁用特定类和包的断言:java -ea:... -da:MyClass MyApp

不能应用没有类加载器的系统类上,要使用:-enablesystemassertions/-esa

使用断言完成参数检查

使用断言的场景:

  • 断言失败是致命的、不可恢复的错误
  • 断言用于开发和测试阶段

不可通告可恢复性的错误,不该作为程序向用户通告问题的手段。

不允许用 null 数组调用这个方法,并在这个方法的开头使用断言:assert a != null;

为文档假设使用断言

if (i % 3 == 0) ...
else if (i % 3 == 1) ...
else //i % 3 ==2 

assert i >= 0;
if (i % 3 == 0) ...
else if (i % 3 == 1) ...
else assert i % 3 ==2;

记录日志

基本日志

使用全局日志记录器并调用 info 方法:

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

内容:

May 10, 2013 10:23:43 PM LoggingImageViewer fileOpen

INFO: File->Open menu item selected

在适当的地方(如 main 开始)以下将会取消所有日志:

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

调试技巧

  1. 打印任意变量的值:System.out.println("x=" + x);
  2. 在每个类中放置一个单独的 main 方法,做单元测试。
  3. 可以使用 JUnit 单元测试框架。

猜你喜欢

转载自www.cnblogs.com/lovezyu/p/9127463.html