7.异常、断言和日志

异常、断言和日志

7.1处理错误

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

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

在这里插入图片描述

Error类层次结构描述了java运行时系统的内部错误和资源耗尽错误。自己的应用程序不应该抛出这种类型的对象。
在设计java程序时,要重点关注Exception层次结构。这个层次结构又分解为两个分支:一个分支派生于RuntimeException;另一个分支包含其他异常。一般规则是:由编程错误导致的异常属于RuntimeException;如果程序本身没有问题,但由于像I/O错误这类问题导致的异常属于其他异常。
Java语言规范将派生于Error类或RuntimeException类的所有异常称为非检查型异常,所有其他的异常称为检查型异常。编译器将检查是否为所有的检查型异常提供了异常处理器。

7.1.2声明检查型异常

在自己编写方法时,不必声明这个方法可能抛出的所有异常。至于什么时候需要在方法中用throws子句声明异常,以及要用throws子句声明哪些异常,需要记住在遇到下面4种情况时会抛出异常:

  • 调用了一个抛出检查型异常的方法,例如,FileInputStream构造器。
  • 检测到一个错误,并且利用throw语句抛出一个检查型异常。
  • 程序出现错误,例如,a[-1] = 0会抛出一个非检查型异常。
  • Java虚拟机或运行时库出现内部错误。

如果出现前两种情况,则必须告诉调用这个方法的程序员有可能抛出异常。因为任何一个抛出异常的方法都有可能是一个死亡陷阱。如果没有处理器捕获这个异常,当前执行的线程就会终止
有些方法包含在对外提供的类中,对于这些方法,应该通过方法首部的异常规范声明这个方法可能抛出异常。如果一个方法有可能抛出多个检查型异常类型,那么就必须在方法的首部列出所有的异常类。
总之,一个方法必须声明所有可能抛出的检查型异常,而非检查型异常要么在控制之外(Error),要么是由从一开始就应该避免的情况所导致的(RuntimeException)。如果方法没有声明所有可能发生的检查型异常,编译器就会发出一个错误消息。
需要注意的是,如果在子类覆盖了超类的一个方法,子类方法中声明的检查型异常不能比超类方法中声明的异常更通用(子类方法可以抛出更特定的异常,或者根本不抛出任何异常)。特别需要说明的是,如果超类方法没有抛出任何检查型异常,子类也不能抛出任何检查型异常
如果类中的一个方法声明它会抛出一个异常,而这个异常是某个特定类的实例,那么这个方法抛出的异常可能属于这个类,也可能属于这个类的任意一个子类。例如,FileInputStream构造器声明有可能抛出一个IOException异常,在这种情况下,并不知道具体是哪种IOException。既可能是IOException,也可能是其某个子类的对象,例如FileNotFoundException

7.2捕获异常
7.2.1捕获异常

要捕获那些知道如何处理的异常,而继续传播那些不知道怎样处理的异常。对于后一种选择,不用感到难堪。将异常交给胜任的处理器进行处理要比压制这个异常更好。同时请记住,这个规则对于继承关系来说会有例外。

7.2.2捕获多个异常

可以捕获多个异常类型,但是,如果异常类型之间存在继承关系,则更通用的需要放在后面。

如果多个异常的处理动作是一样的,可以在一个catch子句中可以捕获多个异常类型:

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

只有当捕获的异常类型彼此之间不存在子类关系时才需要这个特性。捕获多个异常时,异常变量隐含为final变量。

7.2.3再次抛出异常与异常链

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

try {
    // Access the database
} catch (SQLException original) {
    // throw new ServletException("database error: " + original.getMessage());
    // 更好的办法
    var e = new ServletException("database error");
    e.initCause(original);  // 把原始异常设置为新异常的原因
    throw e;
}

// 捕获到这个异常时,可以使用下面这条语句获取原始异常。强烈建议使用这种包装技术。
// 这样可以在子系统中抛出高层异常,而不会丢失原始异常的细节。
Throwable original = caughtException.getCause();

如果在一个方法中发生了一个检查型异常,但这个方法不允许抛出检查型异常,那么包装技术也很有用。可以捕获这个检查型异常,并将它包装成一个运行时异常

7.2.4finally子句

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

var 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语句块中是否遇到异常,finally子句中的代码都会执行。当然,如果真的遇到了一个异常,这个异常将会被重新抛出,并且必须由另一个catch子句捕获:

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

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

需要注意的是,当finally子句包含return语句时,有可能产生意想不到的结果。假设利用return语句从try语句块中间退出。在方法返回前,会执行finally子句块。如果finally块也有一个return语句,这个返回值将会遮蔽原来的返回值,甚至会吞掉异常
因此,finally子句的体要用于清理资源。不要把改变控制流的语句(returnthrowbrreakcontinue)放在finally子句中。

7.2.5try-with-resources语句

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

void close() throws Exception

另外,还有一个Closeable接口。这是AutoCloseable的子接口,也只包含一个close方法。不过,这个方法声明为抛出一个IOException
try-with-resources语句的最简形式为:

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

try正常退出或者存在一个异常时,会自动调用res.close(),就好像使用了finally块一样。还可以指定多个资源:

try (var in = new Scanner(new FileInputStream("/usr/share/dict/words", StandardCharsets.UTF_8)); 
    var out = new PrintWriter("out.txt", StandardCharsets.UTF_8)) {
    while (in.hasNext()) {
        out.println(in.next().toUpperCase());
    }
}
// 不论这个块如何退出,in和out都会关闭

在java9中,可以在try首部中提供之前声明的事实最终变量:

public static void printAll(String[] lines, PrintWriter out) {
    try (out) { // Effectively final variable
        for (String line : lines) {
            out.println(line);
        }
    }   // out.close() called here
}

如果try块抛出一个异常,而且close方法也抛出一个异常,这就会带来一个难题。try-with-resources语句可以很好地处理这种情况。原来的异常会重新抛出,而close方法抛出的异常会被抑制。这些异常将自动捕获,并由addSuppressed方法增加到原来的异常。如果对这些异常感兴趣,可以调用getSuppressed方法,它会生成从close方法抛出并被抑制的异常数组。
try-with-resources语句自身也可以有catch子句,甚至还可以有一个finally子句。这些子句会在关闭资源之后执行

7.3使用异常的技巧
  1. 异常处理不能代替简单的测试。
  2. 不要过分地细化异常。
  3. 充分利用异常层次结构。不要只抛出RuntimeException异常,应该寻找一个适合的子类或创建自己的异常类。不要只捕获Throwable异常,否则,这会使代码更难读、更难维护。
    考虑检查型异常与非检查型异常的区别。检查型异常本来就很庞大,不要为逻辑错误抛出这些异常。
    如果能够将一种异常转换为另一种更加适合的异常,那么不要犹豫。例如,在解析某个文件中的一个整数时,可以捕获NumberFormatException异常,然后将它转换为IOException的一个子类或MySubsystemException
  4. 不要压制异常。
  5. 在检测错误时,苛刻要比放任更好。
  6. 不要羞于传递异常。
7.4使用断言

Java引入了关键字assertassert conditionassert condition : expression。这两个语句都会计算条件,如果结果为false,则抛出一个AssertionError异常。在第二个语句中,表达式将传入AssertionError对象的构造器,并转换一个消息字符串。
断言在以下两种情况不可使用

  • 在对外公开的方法中。防御式编程最核心的一点就是:所有的外部因素(输入参数、环境变量、上下文)都是邪恶的,都存在着企图摧毁程序的罪恶本源,为了抵制它,要在程序中处处检验,满地设卡,不满足条件就不再执行后续程序,以保护主程序的正确性,处处设卡没问题,但就是不能用断言做输入校验,特别是公开方法
  • 在执行逻辑代码的情况下assert的支持是可选的,在开发时可以让它运行,但在生产系统中则不需要其运行了(以便提高性能),因此在assert的布尔表达式中不能执行逻辑代码,否则会因为环境不同而产生不同的逻辑。例如,assert list.remove(element) : " 删除元素 " + element + " 失败";,这段代码在assert启用的环境下,没有任何问题,但是一旦投入到生产环境,就不会启用断言了,而这个方法也就彻底完蛋。

因此,只有按照正常执行逻辑不可能到达的代码区域可以放置assert。具体分为三种情况:

  • 在私有方法中放置assert作为输入参数的校验。在私有方法中可以放置assert校验输入参数,因为私有方法的使用者是作者自己,私有方法的调用者和被调用者之间是一种弱契约关系,或者说没有契约关系,其约束是靠作者自己控制的,因此,加上assert可以更好地预防自己犯错,或者无意的程序犯错。
  • 正常的流程控制中不可能到达的区域
  • 建立程序探针。可能会在一段程序中定义两个变量,分别代表两个不同的业务含义,但是两者之间有固定关系,例如var1 = var2 * 2,就可以在程序中到处设桩,断言这两者的关系,如果不满足则表明程序已经出现了异常,业务也就没必要进行下去了。
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值