《Effective Java Third》第十章总结:异常

本文深入探讨Java异常处理的最佳实践,包括使用异常的正确场景、checked与unchecked异常的区别、异常转换与链式异常的运用、异常详细信息的重要性及故障原子性原则。通过遵循这些指导原则,开发者可以编写更健壮、易于维护的代码。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

https://github.com/clxering/Effective-Java-3rd-edition-Chinese-English-bilingual

第十章 异常

69.仅在确有异常条件下使用异常

利用错误判断机制来提高性能是错误的,将代码放在 try-catch 块中会抑制 JVM 可能执行的某些优化。

事实上,基于异常的用法比标准用法慢得多。

异常只适用于确有异常的情况;它们不应该用于一般的控制流程

一个设计良好的 API 不能迫使其客户端为一般的控制流程使用异常。
只有在某些不可预知的条件下才能调用具有「状态依赖」方法的类,通常应该有一个单独的「状态测试」方法,表明是否适合调用「状态依赖」方法。
例如,Iterator 接口具有「状态依赖」的 next 方法和对应的「状态测试」方法 hasNext。这使得传统 for 循环(在 for-each 循环内部也使用了 hasNext 方法)在集合上进行迭代成为标准习惯用法:

for (Iterator<Foo> i = collection.iterator(); i.hasNext(); ) {
    Foo foo = i.next();
    ...
}

如果 Iterator 缺少 hasNext 方法,客户端将被迫这样做::

// Do not use this hideous code for iteration over a collection!
try {
    Iterator<Foo> i = collection.iterator();
    while(true) {
        Foo foo = i.next();
        ...
    }
}
catch (NoSuchElementException e) {
}

如何在在「状态测试」方法、Optional、可识别的返回值之间进行选择?

(1)如果要在没有外部同步的情况下并发地访问对象,或者受制于外部条件的状态转换,则必须使用 Optional 或可识别的返回值,因为对象的状态可能在调用「状态测试」方法与「状态依赖」方法的间隔中发生变化。

(2)如果一个单独的「状态测试」方法重复「状态依赖」方法的工作,从性能问题考虑,可能要求使用 Optional 或可识别的返回值。

(3)在所有其他条件相同的情况下,「状态测试」方法略优于可识别的返回值。
它提供了较好的可读性,而且不正确的使用可能更容易被检测:
如果你忘记调用「状态测试」方法,「状态依赖」方法将抛出异常,使错误显而易见;

(4)如果你忘记检查一个可识别的返回值,那么这个 bug 可能很难发现。但是这对于返回 Optional 对象的方式来说不是问题。

70 对可恢复情况使用 checked 异常,对编程错误使用运行时异常

Java 提供了三种可抛出项:checked 异常、运行时异常和错误。

决定是使用 checked 异常还是 unchecked 异常的基本规则是:使用 checked 异常的情况是为了合理地期望调用者能够从中恢复。

这个建议存在的问题是,并不总能清楚是在处理可恢复的条件还是编程错误。
例如,考虑资源耗尽的情况,这可能是由编程错误(如分配一个不合理的大数组)或真正的资源短缺造成的。
如果资源枯竭是由于暂时短缺或暂时需求增加造成的,这种情况很可能是可以恢复的。
对于 API 设计人员来说,判断给定的资源耗尽实例是否允许恢复是一个问题。
如果你认为某个条件可能允许恢复,请使用 checked 异常;如果没有,则使用运行时异常。
如果不清楚是否可以恢复,最好使用 unchecked 异常(详见71)

使用运行时异常来指示编程错误

把错误Error保留给 JVM 使用,以指示:资源不足、不可恢复故障或其他导致无法继续执行的条件。
考虑到这种约定被大众认可,所以最好不要实现任何新的 Error 子类。
因此,自定义的所有 unchecked 可抛出项都应该继承 RuntimeException(直接或间接)。
不仅不应该定义 Error 子类,而且除了 AssertionError 之外,不应该抛出它们。

因为 checked 异常通常表示可恢复的条件,所以这类异常来说,设计能够提供信息的方法来帮助调用者从异常条件中恢复尤为重要。
例如,假设当使用礼品卡购物由于资金不足而失败时,抛出一个 checked 异常。该异常应提供一个访问器方法来查询差额。这将使调用者能够将金额传递给购物者。(详见75)

71 避免不必要地使用 checked 异常

如果一个方法抛出 checked 异常,调用它的代码必须在一个或多个 catch 块中处理它们;或者通过声明抛出,让它们向外传播。这给用户带来了负担

如果
(1)正确使用 API 也不能防止异常情况
(2)使用 API 的程序员在遇到异常时可以采取一些有用的操作
那么这种负担是合理的。除非满足这两个条件,否则可以使用 unchecked 异常。

消除 checked 异常的最简单方法是返回所需结果类型的 Optional 对象(55)这种技术的缺点是,该方法不能返回任何详细说明其无法执行所需计算的附加信息。相反,异常具有描述性类型,并且可以导出方法来提供附加信息(70)

还可以通过将抛出异常的方法拆分为两个方法,从而将 checked 异常转换为 unchecked 异常,
第一个方法返回一个布尔值,指示是否将抛出异常,第二个方法为原先的逻辑处理:

// Invocation with checked exception
try {
    obj.action(args);
}
catch (TheCheckedException e) {
    ... // Handle exceptional condition
}

转换为:

// Invocation with state-testing method and unchecked exception
if (obj.actionPermitted(args)) {
    obj.action(args);
}
else {
    ... // Handle exceptional condition
}

72.鼓励复用标准异常

不要直接复用 Exception、RuntimeException、Throwable 或 Error

建议使用如下最常见的可复用异常:

ExceptionOccasion for Use
IllegalArgumentExceptionNon-null parameter value is inappropriate(非空参数值不合适)
IllegalStateExceptionObject state is inappropriate for method invocation(对象状态不适用于方法调用)
NullPointerExceptionParameter value is null where prohibited(禁止参数为空时仍传入 null)
IndexOutOfBoundsExceptionIndex parameter value is out of range(索引参数值超出范围)
ConcurrentModificationExceptionConcurrent modification of an object has been detected where it is prohibited(在禁止并发修改对象的地方检测到该动作)
UnsupportedOperationExceptionObject does not support method(对象不支持该方法调用)

重用必须基于文档化的语义,而不仅仅是基于名称。

异常是可序列化的(Chapter 12)。如果没有充分的理由,不要编写自己的异常类。

73.抛出能用抽象解释的异常

当一个方法抛出一个与它所执行的任务没有明显关联的异常时,这是令人不安的。
这种情况经常发生在由方法传播自低层抽象抛出的异常。
它不仅令人不安,而且让实现细节污染了上层的 API。
如果高层实现在以后的版本中发生变化,那么它抛出的异常也会发生变化,可能会破坏现有的客户端程序。

如果无法防止或处理来自低层的异常,则使用异常转换,但要保证低层方法的所有异常都适用于较高层。
如果低层异常可能有助于调试高层异常的问题,则需要一种称为链式异常的特殊异常转换形式;
链式异常提供了兼顾两方面的最佳服务:允许抛出适当的高层异常,同时捕获并分析失败的潜在原因

异常转换:高层应该捕获低层异常,并确保抛出的异常可以用高层抽象解释

// 异常转换
try {
    ... // 使用较低层次的抽象来完成我们的任务
} catch (LowerLevelException e) {
    throw new HigherLevelException(...);
}

链式异常:低层异常(作为原因)传递给高层异常,高层异常提供一个访问器方法(Throwable 的 getCause 方法)来检索低层异常

// Exception Chaining
try {
    ... // Use lower-level abstraction to do our bidding
}
catch (LowerLevelException cause) {
    throw new HigherLevelException(cause);
}

高层异常的构造函数将原因传递给能够接收链式异常的超类构造函数,因此它最终被传递给 Throwable 的一个接收链式异常的构造函数,比如 Throwable(Throwable)

// Exception with chaining-aware constructor
class HigherLevelException extends Exception {
    HigherLevelException(Throwable cause) {
        super(cause);
    }
}

74.为每个方法记录会抛出的所有异常

描述方法抛出的异常,是该方法文档的重要部分。因此,花时间仔细记录每个方法抛出的所有异常是非常重要的(56)

始终单独声明 checked 异常,并使用 Javadoc 的 @throw 标记精确记录每次抛出异常的条件

使用 Javadoc 的 @throw 标记记录方法会抛出的每个异常,但是不要对 unchecked 异常使用 throws 关键字
让使用你的 API 的程序员知道哪些异常是 checked 异常,哪些是 unchecked 异常是很重要的,因为程序员在这两种情况下的职责是不同的。
Javadoc 的 @throw 标记生成的文档在方法声明中没有对应的 throws 子句,这向程序员提供了一个强烈的视觉提示,这是 unchecked 异常。

如果一个类中的许多方法都因为相同的原因抛出异常,你可以在类的文档注释中记录异常,而不是为每个方法单独记录异常

总结:
记录你所编写的每个方法可能引发的每个异常。
对于 unchecked 异常、checked 异常、抽象方法、实例方法都是如此。
应该在文档注释中采用 @throw 标记的形式。在方法的 throws 子句中分别声明每个 checked 异常,但不要声明 unchecked 异常。
如果你不记录方法可能抛出的异常,其他人将很难或不可能有效地使用你的类和接口。

75.异常详细消息中应包含捕获失败的信息

异常的 toString 方法返回尽可能多的关于失败原因的信息是非常重要的。
换句话说,由失败导致的异常的详细信息应该被捕获,以便后续分析。

异常的详细消息应该包含导致异常的所有参数和字段的值

不应包含密码、加密密钥等详细信息。

确保异常在其详细信息中包含足够的故障捕获信息的一种方法是:在其构造函数中配置,而不是以传入字符串方式引入这些信息。之后可以自动生成详细信息来包含细节;
如IndexOutOfBoundsException 构造函数不包含 String 参数:

public IndexOutOfBoundsException(int lowerBound, int upperBound, int index) {
    // 生成捕获故障的详细信息
    super(String.format("Lower bound: %d, Upper bound: %d, Index: %d",lowerBound, upperBound, index));
    // 为编程访问保存故障信息s
    this.lowerBound = lowerBound;
    this.upperBound = upperBound;
    this.index = index;
}

76.尽力保证故障原子性

一般来说,一个失败的方法调用应该使对象处于调用之前的状态,具有此属性的方法称为具备故障原子性。

1 最简单的方法是设计不可变对象(17)

2 而对于操作可变对象的方法,实现故障原子性的最常见方法是在执行操作之前检查参数的有效性(49);
这使得大多数异常在对象修改开始之前被抛出。

如Stack#pop:

public Object pop() {
    if (size == 0)
        throw new EmptyStackException();
    Object result = elements[--size];
    elements[size] = null; // Eliminate obsolete reference
    return result;
}

如果取消了初始大小检查,当该方法试图从空堆栈中弹出元素时,仍然会抛出异常。
但是,这会使 size 字段处于不一致的(负值)状态,导致以后该对象的任何方法调用都会失败。
此外,pop 方法抛出的 ArrayIndexOutOfBoundsException 也不适于高层抽象解释(73)

3 对计算进行排序,以便可能发生故障的部分都先于修改对象的部分发生。

如TreeMap ,它的元素按照一定的顺序排序。
为了向 TreeMap 中添加元素,元素的类型必须能够使用 TreeMap 的顺序进行比较。
在以任何方式修改树之前,尝试添加类型不正确的元素就会失败,因为搜索树中的元素会产生ClassCastException。

4 以对象的临时副本执行操作,并在操作完成后用临时副本替换对象的内容

例如,一些排序函数在排序之前将其输入 list 复制到数组中,以降低访问排序内循环中的元素的成本。这样做是为了提高性能,但是作为一个额外的好处,它确保如果排序失败,输入 list 将保持不变。

5 编写恢复代码,拦截在操作过程中发生的故障,并使对象回滚到操作开始之前的状态。

这种方法主要用于持久的(基于磁盘的)数据结构。

当然,对于某些操作,它将显著增加成本或复杂性

且并不总是可以实现的:例如,如果两个线程试图在没有适当同步的情况下并发地修改同一个对象,那么该对象可能会处于不一致的状态。因此,如果假定在捕捉到 ConcurrentModificationException 之后对象仍然可用,那就错了。该错误是不可恢复的,所以在抛出 AssertionError 时,甚至不需要尝试保存故障原子性。

所以如果违反了这条规则,API 文档应该清楚地指出对象将处于什么状态。

77.不要忽略异常

如果在方法调用的周围加上一条 try 语句,其 catch 块为空,可以很容易忽略异常

空 catch 块违背了异常的目的

在某些情况下,忽略异常是合适的。
例如,当关闭 FileInputStream 时,它可能是适当的。
假设你没有更改文件的状态,那么不需要执行任何恢复操作,而且你已经从文件中读取了所需的信息,因此没有理由中止正在进行的操作。
记录异常可能是明智的,这样就可以调查这些异常是否经常发生。
如果你选择忽略一个异常,catch 块应该包含一个注释,解释为什么这样做是合适的,并且变量应该被命名为忽略 ignore

此建议适用于 checked 异常和 unchecked异常

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值