第 10 章 异常

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

Use exceptions only for exceptional conditions

Someday, if you are unlucky, you may stumble across a piece of code that looks something like this:

你可能会偶然发现这样一段代码:

  1. // Horrible abuse of exceptions. Don't ever do this!
  2. try {
  3. int i = 0;
  4. while(true)
  5. range[i++].climb();
  6. }
  7. catch (ArrayIndexOutOfBoundsException e) {
  8. }

What does this code do? It’s not at all obvious from inspection, and that’s reason enough not to use it (Item 67). It turns out to be a horribly ill-conceived idiom for looping through the elements of an array. The infinite loop terminates by throwing, catching, and ignoring an ArrayIndexOutOfBoundsException when it attempts to access the first array element outside the bounds of the array. It’s supposed to be equivalent to the standard idiom for looping through an array, which is instantly recognizable to any Java programmer:

这段代码是做什么的?从表面上看,一点也不明显,这足以成为不使用它的充分理由(Item-67)。事实证明,这是一个用于遍历数组的元素的非常糟糕的习惯用法。当试图访问数组边界之外的数组元素时,通过抛出、捕获和忽略 ArrayIndexOutOfBoundsException 来终止无限循环。如下循环遍历数组的标准习惯用法,任何 Java 程序员都可以立即识别它:

  1. for (Mountain m : range)
  2. m.climb();

So why would anyone use the exception-based loop in preference to the tried and true? It’s a misguided attempt to improve performance based on the faulty reasoning that, since the VM checks the bounds of all array accesses, the normal loop termination test—hidden by the compiler but still present in the for-each loop—is redundant and should be avoided. There are three things wrong with this reasoning:

那么,为什么会有人使用基于异常的循环而不使用习惯的循环模式呢?由于 VM 检查所有数组访问的边界,所以误认为正常的循环终止测试被编译器隐藏了,但在 for-each 循环中仍然可见,这无疑是多余的,应该避免,因此利用错误判断机制来提高性能是错误的。这种思路有三点误区:

  • Because exceptions are designed for exceptional circumstances, there is little incentive for JVM implementors to make them as fast as explicit tests.

因为异常是为特殊情况设计的,所以 JVM 实现几乎不会让它们像显式测试一样快。

  • Placing code inside a try-catch block inhibits certain optimizations that JVM implementations might otherwise perform.

将代码放在 try-catch 块中会抑制 JVM 可能执行的某些优化。

  • The standard idiom for looping through an array doesn’t necessarily result in redundant checks. Many JVM implementations optimize them away.

遍历数组的标准习惯用法不一定会导致冗余检查。许多 JVM 实现对它们进行了优化。

In fact, the exception-based idiom is far slower than the standard one. On my machine, the exception-based idiom is about twice as slow as the standard one for arrays of one hundred elements.

事实上,基于异常的用法比标准用法慢得多。在我的机器上,用 100 个元素的数组测试,基于异常的用法与标准用法相比速度大约慢了两倍。

Not only does the exception-based loop obfuscate the purpose of the code and reduce its performance, but it’s not guaranteed to work. If there is a bug in the loop, the use of exceptions for flow control can mask the bug, greatly complicating the debugging process. Suppose the computation in the body of the loop invokes a method that performs an out-of-bounds access to some unrelated array. If a reasonable loop idiom were used, the bug would generate an uncaught exception, resulting in immediate thread termination with a full stack trace. If the misguided exception-based loop were used, the bug-related exception would be caught and misinterpreted as a normal loop termination.

基于异常的循环不仅混淆了代码的目的,降低了代码的性能,而且不能保证它能正常工作。如果循环中存在 bug,使用异常进行流程控制会掩盖该 bug,从而大大增加调试过程的复杂性。假设循环体中的计算步骤调用一个方法,该方法对一些不相关的数组执行越界访问。如果使用合理的循环习惯用法,该 bug 将生成一个未捕获的异常,导致线程立即终止,并带有完整的堆栈跟踪。相反,如果使用了基于异常的循环,当捕获与 bug 相关的异常时,会将其误判为正常的循环终止条件。

The moral of this story is simple: Exceptions are, as their name implies, to be used only for exceptional conditions; they should never be used for ordinary control flow. More generally, use standard, easily recognizable idioms in preference to overly clever techniques that purport to offer better performance. Even if the performance advantage is real, it may not remain in the face of steadily improving platform implementations. The subtle bugs and maintenance headaches that come from overly clever techniques, however, are sure to remain.

这个案例的寓意很简单:顾名思义,异常只适用于确有异常的情况;它们不应该用于一般的控制流程。 更进一步说,使用标准的、易于识别的习惯用法,而不是声称能够提供更好性能的过于抖机灵的技术。即使性能优势是真实存在的,但在稳步改进平台实现的前提下,这种优势也并不可靠。而且,来自抖机灵的技术存在的细微缺陷和维护问题肯定会继续存在。

This principle also has implications for API design. A well-designed API must not force its clients to use exceptions for ordinary control flow. A class with a “state-dependent” method that can be invoked only under certain unpredictable conditions should generally have a separate “state-testing” method indicating whether it is appropriate to invoke the state-dependent method. For example, the Iterator interface has the state-dependent method next and the corresponding state-testing method hasNext. This enables the standard idiom for iterating over a collection with a traditional for loop (as well as the for-each loop, where the hasNext method is used internally):

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

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

If Iterator lacked the hasNext method, clients would be forced to do this instead:

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

  1. // Do not use this hideous code for iteration over a collection!
  2. try {
  3. Iterator<Foo> i = collection.iterator();
  4. while(true) {
  5. Foo foo = i.next();
  6. ...
  7. }
  8. }
  9. catch (NoSuchElementException e) {
  10. }

This should look very familiar after the array iteration example that began this item. In addition to being wordy and misleading, the exception-based loop is likely to perform poorly and can mask bugs in unrelated parts of the system.

这与一开始举例的对数组进行迭代的例子非常相似,除了冗长和误导之外,基于异常的循环执行效果可能很差,并且会掩盖系统中不相关部分的 bug。

An alternative to providing a separate state-testing method is to have the statedependent method return an empty optional (Item 55) or a distinguished value such as null if it cannot perform the desired computation.

提供单独的「状态测试」方法的另一种方式,就是让「状态依赖」方法返回一个空的 Optional 对象(Item-55),或者在它不能执行所需的计算时返回一个可识别的值,比如 null。

Here are some guidelines to help you choose between a state-testing method and an optional or distinguished return value. If an object is to be accessed concurrently without external synchronization or is subject to externally induced state transitions, you must use an optional or distinguished return value, as the object’s state could change in the interval between the invocation of a state-testing method and its state-dependent method. Performance concerns may dictate that an optional or distinguished return value be used if a separate statetesting method would duplicate the work of the state-dependent method. All other things being equal, a state-testing method is mildly preferable to a distinguished return value. It offers slightly better readability, and incorrect use may be easier to detect: if you forget to call a state-testing method, the statedependent method will throw an exception, making the bug obvious; if you forget to check for a distinguished return value, the bug may be subtle. This is not an issue for optional return values.

有一些指导原则,帮助你在「状态测试」方法、Optional、可识别的返回值之间进行选择。(1)如果要在没有外部同步的情况下并发地访问对象,或者受制于外部条件的状态转换,则必须使用 Optional 或可识别的返回值,因为对象的状态可能在调用「状态测试」方法与「状态依赖」方法的间隔中发生变化。(2)如果一个单独的「状态测试」方法重复「状态依赖」方法的工作,从性能问题考虑,可能要求使用 Optional 或可识别的返回值。(3)在所有其他条件相同的情况下,「状态测试」方法略优于可识别的返回值。它提供了较好的可读性,而且不正确的使用可能更容易被检测:如果你忘记调用「状态测试」方法,「状态依赖」方法将抛出异常,使错误显而易见;(4)如果你忘记检查一个可识别的返回值,那么这个 bug 可能很难发现。但是这对于返回 Optional 对象的方式来说不是问题。

In summary, exceptions are designed for exceptional conditions. Don’t use them for ordinary control flow, and don’t write APIs that force others to do so.

总之,异常是为确有异常的情况设计的。不要将它们用于一般的控制流程,也不要编写强制其他人这样做的 API。


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

Use checked exceptions for recoverable conditions and runtime exceptions for programming errors

Java provides three kinds of throwables: checked exceptions, runtime exceptions, and errors. There is some confusion among programmers as to when it is appropriate to use each kind of throwable. While the decision is not always clear-cut, there are some general rules that provide strong guidance.

Java 提供了三种可抛出项:checked 异常、运行时异常和错误。程序员们对什么时候使用这些可抛出项比较困惑。虽然决策并不总是明确的,但是有一些通用规则可以提供强有力的指导。

The cardinal rule in deciding whether to use a checked or an unchecked exception is this: use checked exceptions for conditions from which the caller can reasonably be expected to recover. By throwing a checked exception, you force the caller to handle the exception in a catch clause or to propagate it outward. Each checked exception that a method is declared to throw is therefore a potent indication to the API user that the associated condition is a possible outcome of invoking the method.

决定是使用 checked 异常还是 unchecked 异常的基本规则是:使用 checked 异常的情况是为了合理地期望调用者能够从中恢复。 通过抛出一个 checked 的异常,你可以强制调用者在 catch 子句中处理异常,或者将其传播出去。因此,方法中声明的要抛出的每个 checked 异常,都清楚的向 API 用户表明:the associated condition is a possible outcome of invoking the method.

By confronting the user with a checked exception, the API designer presents a mandate to recover from the condition. The user can disregard the mandate by catching the exception and ignoring it, but this is usually a bad idea (Item 77).

通过向用户提供 checked 异常,API 设计者提供了从条件中恢复的要求。用户为了无视强制要求,可以捕获异常并忽略,但这通常不是一个好主意(Item-77) 。

There are two kinds of unchecked throwables: runtime exceptions and errors. They are identical in their behavior: both are throwables that needn’t, and generally shouldn’t, be caught. If a program throws an unchecked exception or an error, it is generally the case that recovery is impossible and continued execution would do more harm than good. If a program does not catch such a throwable, it will cause the current thread to halt with an appropriate error message.

有两种 unchecked 的可抛出项:运行时异常和错误。它们在行为上是一样的:都是可抛出的,通常不需要也不应该被捕获。如果程序抛出 unchecked 异常或错误,通常情况下是不可能恢复的,如果继续执行,弊大于利。如果程序没有捕获到这样的可抛出项,它将导致当前线程停止,并发出适当的错误消息。

Use runtime exceptions to indicate programming errors. The great majority of runtime exceptions indicate precondition violations. A precondition violation is simply a failure by the client of an API to adhere to the contract established by the API specification. For example, the contract for array access specifies that the array index must be between zero and the array length minus one, inclusive. ArrayIndexOutOfBoundsException indicates that this precondition was violated.

使用运行时异常来指示编程错误。 绝大多数运行时异常都表示操作违反了先决条件。违反先决条件是指使用 API 的客户端未能遵守 API 规范所建立的约定。例如,数组访问约定指定数组索引必须大于等于 0 并且小于等于 length-1 (length:数组长度)。ArrayIndexOutOfBoundsException 表示违反了此先决条件。

One problem with this advice is that it is not always clear whether you’re dealing with a recoverable conditions or a programming error. For example, consider the case of resource exhaustion, which can be caused by a programming error such as allocating an unreasonably large array, or by a genuine shortage of resources. If resource exhaustion is caused by a temporary shortage or by temporarily heightened demand, the condition may well be recoverable. It is a matter of judgment on the part of the API designer whether a given instance of resource exhaustion is likely to allow for recovery. If you believe a condition is likely to allow for recovery, use a checked exception; if not, use a runtime exception. If it isn’t clear whether recovery is possible, you’re probably better off using an unchecked exception, for reasons discussed in Item 71.

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

While the Java Language Specification does not require it, there is a strong convention that errors are reserved for use by the JVM to indicate resource deficiencies, invariant failures, or other conditions that make it impossible to continue execution. Given the almost universal acceptance of this convention, it’s best not to implement any new Error subclasses. Therefore, all of the unchecked throwables you implement should subclass RuntimeException (directly or indirectly). Not only shouldn’t you define Error subclasses, but with the exception of AssertionError, you shouldn’t throw them either.

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

It is possible to define a throwable that is not a subclass of Exception, RuntimeException, or Error. The JLS doesn’t address such throwables directly but specifies implicitly that they behave as ordinary checked exceptions (which are subclasses of Exception but not RuntimeException). So when should you use such a beast? In a word, never. They have no benefits over ordinary checked exceptions and would serve merely to confuse the user of your API.

可以自定义一种可抛出的异常,它不是 Exception、RuntimeException 或 Error 的子类。JLS 不直接处理这些可抛出项,而是隐式地指定它们作为普通 checked 异常(普通 checked 异常是 Exception 的子类,但不是 RuntimeException 的子类)。那么,什么时候应该使用这样的「猛兽」呢?总之,永远不要。与普通 checked 异常相比,它们没有任何好处,只会让 API 的用户感到困惑。

API designers often forget that exceptions are full-fledged objects on which arbitrary methods can be defined. The primary use of such methods is to provide code that catches the exception with additional information concerning the condition that caused the exception to be thrown. In the absence of such methods, programmers have been known to parse the string representation of an exception to ferret out additional information. This is extremely bad practice (Item 12). Throwable classes seldom specify the details of their string representations, so string representations can differ from implementation to implementation and release to release. Therefore, code that parses the string representation of an exception is likely to be nonportable and fragile.

API 设计人员常常忘记异常是成熟对象,可以为其定义任意方法。此类方法的主要用途是提供捕获异常的代码,并提供有关引发异常的附加信息。如果缺乏此类方法,程序员需要自行解析异常的字符串表示以获取更多信息。这是极坏的做法(Item-12)。这种类很少指定其字符串表示的细节,因此字符串表示可能因实现而异,也可能因版本而异。因此,解析异常的字符串表示形式的代码可能是不可移植且脆弱的。

Because checked exceptions generally indicate recoverable conditions, it’s especially important for them to provide methods that furnish information to help the caller recover from the exceptional condition. For example, suppose a checked exception is thrown when an attempt to make a purchase with a gift card fails due to insufficient funds. The exception should provide an accessor method to query the amount of the shortfall. This will enable the caller to relay the amount to the shopper. See Item 75 for more on this topic.

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

To summarize, throw checked exceptions for recoverable conditions and unchecked exceptions for programming errors. When in doubt, throw unchecked exceptions. Don’t define any throwables that are neither checked exceptions nor runtime exceptions. Provide methods on your checked exceptions to aid in recovery.

总而言之,为可恢复条件抛出 checked 异常,为编程错误抛出 unchecked 异常。当有疑问时,抛出 unchecked 异常。不要定义任何既不是 checked 异常也不是运行时异常的自定义异常。应该为 checked 异常设计相关的方法,如提供异常信息,以帮助恢复。


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

Avoid unnecessary use of checked exceptions

Many Java programmers dislike checked exceptions, but used properly, they can improve APIs and programs. Unlike return codes and unchecked exceptions, they force programmers to deal with problems, enhancing reliability. That said, overuse of checked exceptions in APIs can make them far less pleasant to use. If a method throws checked exceptions, the code that invokes it must handle them in one or more catch blocks, or declare that it throws them and let them propagate outward. Either way, it places a burden on the user of the API. The burden increased in Java 8, as methods throwing checked exceptions can’t be used directly in streams (Items 45–48).

许多 Java 程序员不喜欢 checked 异常,但是如果使用得当,它们可以有利于 API 和程序。与返回代码和 unchecked 异常不同,它们强制程序员处理问题,提高了可靠性。相反,在 API 中过度使用 checked 异常会变得不那么令人愉快。如果一个方法抛出 checked 异常,调用它的代码必须在一个或多个 catch 块中处理它们;或者通过声明抛出,让它们向外传播。无论哪种方式,它都给 API 的用户带来了负担。Java 8 中,这一负担增加得更多,因为会抛出 checked 异常的方法不能直接在流(Item 45-48)中使用。

This burden may be justified if the exceptional condition cannot be prevented by proper use of the API and the programmer using the API can take some useful action once confronted with the exception. Unless both of these conditions are met, an unchecked exception is appropriate. As a litmus test, ask yourself how the programmer will handle the exception. Is this the best that can be done?

如果(1)正确使用 API 也不能防止异常情况,(2)并且使用 API 的程序员在遇到异常时可以采取一些有用的操作,那么这种负担是合理的。除非满足这两个条件,否则可以使用 unchecked 异常。作为程序能否成功的试金石,程序员应该问问自己将如何处理异常。这是最好的办法吗?

  1. } catch (TheCheckedException e) {
  2. throw new AssertionError(); // Can't happen!
  3. }

Or this?

或者这样?

  1. } catch (TheCheckedException e) {
  2. e.printStackTrace(); // Oh well, we lose.
  3. System.exit(1);
  4. }

If the programmer can do no better, an unchecked exception is called for.

如果程序员不能做得更好,则需要一个 unchecked 异常。

The additional burden on the programmer caused by a checked exception is substantially higher if it is the sole checked exception thrown by a method. If there are others, the method must already appear in a try block, and this exception requires, at most, another catch block. If a method throws a single checked exception, this exception is the sole reason the method must appear in a try block and can’t be used directly in streams. Under these circumstances, it pays to ask yourself if there is a way to avoid the checked exception.

如果 checked 异常是方法抛出的唯一 checked 异常,那么 checked 异常给程序员带来的额外负担就会大得多。如果还有其他方法,则该方法必须已经出现在 try 块中,并且此异常最多需要另一个 catch 块。如果一个方法抛出一个 checked 异常,那么这个异常就是该方法必须出现在 try 块中而不能直接在流中使用的唯一原因。在这种情况下,有必要问问自己是否有办法避免 checked 异常。

The easiest way to eliminate a checked exception is to return an optional of the desired result type (Item 55). Instead of throwing a checked exception, the method simply returns an empty optional. The disadvantage of this technique is that the method can’t return any additional information detailing its inability to perform the desired computation. Exceptions, by contrast, have descriptive types, and can export methods to provide additional information (Item 70).

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

You can also turn a checked exception into an unchecked exception by breaking the method that throws the exception into two methods, the first of which returns a boolean indicating whether the exception would be thrown. This API refactoring transforms the calling sequence from this:

你还可以通过将抛出异常的方法拆分为两个方法,从而将 checked 异常转换为 unchecked 异常,第一个方法返回一个布尔值,指示是否将抛出异常。这个 API 重构将调用序列转换为:

  1. // Invocation with checked exception
  2. try {
  3. obj.action(args);
  4. }
  5. catch (TheCheckedException e) {
  6. ... // Handle exceptional condition
  7. }

into this:

转换为这种形式:

  1. // Invocation with state-testing method and unchecked exception
  2. if (obj.actionPermitted(args)) {
  3. obj.action(args);
  4. }
  5. else {
  6. ... // Handle exceptional condition
  7. }

This refactoring is not always appropriate, but where it is, it can make an API more pleasant to use. While the latter calling sequence is no prettier than the former, the refactored API is more flexible. If the programmer knows the call will succeed, or is content to let the thread terminate if it fails, the refactoring also allows this trivial calling sequence:

这种重构并不总是适当的,但是只要在适当的地方,它就可以使 API 更易于使用。虽然后一种调用序列并不比前一种调用序列漂亮,但是经过重构的 API 更加灵活。如果程序员知道调用会成功,或者不介意由于调用失败而导致的线程终止,那么该重构还可以接受更简单的调用序列:

  1. obj.action(args);

If you suspect that the trivial calling sequence will be the norm, then the API refactoring may be appropriate. The resulting API is essentially the state-testing method API in Item 69 and the same caveats apply: if an object is to be accessed concurrently without external synchronization or it is subject to externally induced state transitions, this refactoring is inappropriate because the object’s state may change between the calls to actionPermitted and action. If a separate actionPermitted method would duplicate the work of the action method, the refactoring may be ruled out on performance grounds.

If you suspect that the trivial calling sequence will be the norm,那么 API 重构可能是合适的。重构之后的 API 在本质上等同于 Item-69 中的「状态测试」方法,并且,也有同样的告诫:如果对象将在缺少外部同步的情况下被并发访问,或者可被外界改变状态,这种重构就是不恰当的,因为在 actionPermitted 和 action 这两个调用的间隔,对象的状态有可能会发生变化。如果单独的 actionPermitted 方法必须重复 action 方法的工作,出于性能的考虑,这种 API 重构就不值得去做。

In summary, when used sparingly, checked exceptions can increase the reliability of programs; when overused, they make APIs painful to use. If callers won’t be able to recover from failures, throw unchecked exceptions. If recovery may be possible and you want to force callers to handle exceptional conditions, first consider returning an optional. Only if this would provide insufficient information in the case of failure should you throw a checked exception.

总之,如果谨慎使用,checked 异常可以提高程序的可靠性;当过度使用时,它们会使 API 难以使用。如果调用者不应从失败中恢复,则抛出 unchecked 异常。如果恢复是可能的,并且你希望强制调用者处理异常条件,那么首先考虑返回一个 Optional 对象。只有当在失败的情况下,提供的信息不充分时,你才应该抛出一个 checked 异常。


【72】鼓励复用标准异常

Favor the use of standard exceptions

An attribute that distinguishes expert programmers from less experienced ones is that experts strive for and usually achieve a high degree of code reuse. Exceptions are no exception to the rule that code reuse is a good thing. The Java libraries provide a set of exceptions that covers most of the exception-throwing needs of most APIs.

专家程序员与经验较少的程序员之间的一个区别是,专家力求实现高度的代码复用。代码复用是一件好事,异常也不例外。Java 库提供了一组异常,涵盖了大多数 API 的大多数异常抛出需求。

Reusing standard exceptions has several benefits. Chief among them is that it makes your API easier to learn and use because it matches the established conventions that programmers are already familiar with. A close second is that programs using your API are easier to read because they aren’t cluttered with unfamiliar exceptions. Last (and least), fewer exception classes means a smaller memory footprint and less time spent loading classes.

复用标准异常有几个好处。其中最主要的是,它使你的 API 更容易学习和使用,因为它符合程序员已经熟悉的既定约定。其次,使用你的 API 的程序更容易阅读,因为它们不会因为不熟悉的异常而混乱。最后(也是最不重要的),更少的异常类意味着更小的内存占用和更少的加载类的时间。

The most commonly reused exception type is IllegalArgumentException (Item 49). This is generally the exception to throw when the caller passes in an argument whose value is inappropriate. For example, this would be the exception to throw if the caller passed a negative number in a parameter representing the number of times some action was to be repeated.

最常见的复用异常类型是 IllegalArgumentException(Item-49)。这通常是调用者传入不合适的参数时抛出的异常。例如,如果调用者在表示某个操作要重复多少次的参数中传递了一个负数,则抛出这个异常。

Another commonly reused exception is IllegalStateException. This is generally the exception to throw if the invocation is illegal because of the state of the receiving object. For example, this would be the exception to throw if the caller attempted to use some object before it had been properly initialized.

另一个常被复用异常是 IllegalStateException。如果接收对象的状态导致调用非法,则通常会抛出此异常。例如,如果调用者试图在对象被正确初始化之前使用它,那么这将是抛出的异常。

Arguably, every erroneous method invocation boils down to an illegal argument or state, but other exceptions are standardly used for certain kinds of illegal arguments and states. If a caller passes null in some parameter for which null values are prohibited, convention dictates that NullPointerException be thrown rather than IllegalArgumentException. Similarly, if a caller passes an out-of-range value in a parameter representing an index into a sequence, IndexOutOfBoundsException should be thrown rather than IllegalArgumentException.

可以说,每个错误的方法调用都归结为参数非法或状态非法,但是有一些异常通常用于某些特定的参数非法和状态非法。如果调用者在禁止空值的参数中传递 null,那么按照惯例,抛出 NullPointerException 而不是 IllegalArgumentException。类似地,如果调用者将表示索引的参数中的超出范围的值传递给序列,则应该抛出 IndexOutOfBoundsException,而不是 IllegalArgumentException。

Another reusable exception is ConcurrentModificationException. It should be thrown if an object that was designed for use by a single thread (or with external synchronization) detects that it is being modified concurrently. This exception is at best a hint because it is impossible to reliably detect concurrent modification.

另一个可复用异常是 ConcurrentModificationException。如果一个对象被设计为由单个线程使用(或与外部同步),并且检测到它正在被并发地修改,则应该抛出该异常。因为不可能可靠地检测并发修改,所以该异常充其量只是一个提示。

A last standard exception of note is UnsupportedOperationException. This is the exception to throw if an object does not support an attempted operation. Its use is rare because most objects support all of their methods. This exception is used by classes that fail to implement one or more optional operations defined by an interface they implement. For example, an append-only List implementation would throw this exception if someone tried to delete an element from the list.

最后一个需要注意的标准异常是 UnsupportedOperationException。如果对象不支持尝试的操作,则抛出此异常。它很少使用,因为大多数对象都支持它们的所有方法。此异常用于一个类没有实现由其实现的接口定义的一个或多个可选操作。例如,对于只支持追加操作的 List 实现,试图从中删除元素时就会抛出这个异常。

Do not reuse Exception, RuntimeException, Throwable, or Error directly. Treat these classes as if they were abstract. You can’t reliably test for these exceptions because they are superclasses of other exceptions that a method may throw.

不要直接复用 Exception、RuntimeException、Throwable 或 Error。 应当将这些类视为抽象类。你不能对这些异常进行可靠的测试,因为它们是方法可能抛出的异常的超类。

This table summarizes the most commonly reused exceptions:

此表总结了最常见的可复用异常:

Exception Occasion for Use
IllegalArgumentException Non-null parameter value is inappropriate(非空参数值不合适)
IllegalStateException Object state is inappropriate for method invocation(对象状态不适用于方法调用)
NullPointerException Parameter value is null where prohibited(禁止参数为空时仍传入 null)
IndexOutOfBoundsException Index parameter value is out of range(索引参数值超出范围)
ConcurrentModificationException Concurrent modification of an object has been detected where it is prohibited(在禁止并发修改对象的地方检测到该动作)
UnsupportedOperationException Object does not support method(对象不支持该方法调用)

While these are by far the most commonly reused exceptions, others may be reused where circumstances warrant. For example, it would be appropriate to reuse ArithmeticException and NumberFormatException if you were implementing arithmetic objects such as complex numbers or rational numbers. If an exception fits your needs, go ahead and use it, but only if the conditions under which you would throw it are consistent with the exception’s documentation: reuse must be based on documented semantics, not just on name. Also, feel free to subclass a standard exception if you want to add more detail (Item 75), but remember that exceptions are serializable (Chapter 12). That alone is reason not to write your own exception class without good reason.

虽然到目前为止,这些是最常见的复用异常,但是在环境允许的情况下也可以复用其他异常。例如,如果你正在实现诸如复数或有理数之类的算术对象,那么复用 ArithmeticException 和 NumberFormatException 是合适的。如果一个异常符合你的需要,那么继续使用它,但前提是你抛出它的条件与异常的文档描述一致:复用必须基于文档化的语义,而不仅仅是基于名称。另外,如果你想添加更多的细节,可以随意子类化标准异常(第 75 项),但是请记住,异常是可序列化的(Chapter 12)。如果没有充分的理由,不要编写自己的异常类。

Choosing which exception to reuse can be tricky because the “occasions for use” in the table above do not appear to be mutually exclusive. Consider the case of an object representing a deck of cards, and suppose there were a method to deal a hand from the deck that took as an argument the size of the hand. If the caller passed a value larger than the number of cards remaining in the deck, it could be construed as an IllegalArgumentException (the handSize parameter value is too high) or an IllegalStateException (the deck contains too few cards). Under these circumstances, the rule is to throw IllegalStateException if no argument values would have worked, otherwise throw IllegalArgumentException.

选择复用哪个异常可能比较棘手,因为上表中的「使用场合」似乎并不相互排斥。考虑一个对象,表示一副牌,假设有一个方法代表发牌操作,该方法将手牌多少作为参数。如果调用者传递的值大于牌堆中剩余的牌的数量,则可以将其解释为 IllegalArgumentException (handSize 参数值太大)或 IllegalStateException(牌堆中包含的牌太少)。在这种情况下,规则是:如果没有参数值,抛出 IllegalStateException,否则抛出 IllegalArgumentException。


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

Throw exceptions appropriate to the abstraction

It is disconcerting when a method throws an exception that has no apparent connection to the task that it performs. This often happens when a method propagates an exception thrown by a lower-level abstraction. Not only is it disconcerting, but it pollutes the API of the higher layer with implementation details. If the implementation of the higher layer changes in a later release, the exceptions it throws will change too, potentially breaking existing client programs.

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

To avoid this problem, higher layers should catch lower-level exceptions and, in their place, throw exceptions that can be explained in terms of the higher-level abstraction. This idiom is known as exception translation:

为了避免这个问题,高层应该捕获低层异常,并确保抛出的异常可以用高层抽象解释。 这个习惯用法称为异常转换:

  1. // Exception Translation
  2. try {
  3. ... // Use lower-level abstraction to do our bidding
  4. } catch (LowerLevelException e) {
  5. throw new HigherLevelException(...);
  6. }

Here is an example of exception translation taken from the AbstractSequentialList class, which is a skeletal implementation (Item 20) of the List interface. In this example, exception translation is mandated by the specification of the get method in the List<E> interface:

下面是来自 AbstractSequentialList 类的异常转换示例,该类是 List 接口的一个框架实现(Item-20)。在本例中,异常转换是由 List<E> 接口中的 get 方法规范强制执行的:

  1. /**
  2. * Returns the element at the specified position in this list.
  3. * @throws IndexOutOfBoundsException if the index is out of range
  4. * ({@code index < 0 || index >= size()}).
  5. */
  6. public E get(int index) {
  7. ListIterator<E> i = listIterator(index);
  8. try {
  9. return i.next();
  10. }
  11. catch (NoSuchElementException e) {
  12. throw new IndexOutOfBoundsException("Index: " + index);
  13. }
  14. }

A special form of exception translation called exception chaining is called for in cases where the lower-level exception might be helpful to someone debugging the problem that caused the higher-level exception. The lower-level exception (the cause) is passed to the higher-level exception, which provides an accessor method (Throwable’s getCause method) to retrieve the lower-level exception:

如果低层异常可能有助于调试高层异常的问题,则需要一种称为链式异常的特殊异常转换形式。低层异常(作为原因)传递给高层异常,高层异常提供一个访问器方法(Throwable 的 getCause 方法)来检索低层异常:

  1. // Exception Chaining
  2. try {
  3. ... // Use lower-level abstraction to do our bidding
  4. }
  5. catch (LowerLevelException cause) {
  6. throw new HigherLevelException(cause);
  7. }

The higher-level exception’s constructor passes the cause to a chaining-aware superclass constructor, so it is ultimately passed to one of Throwable’s chaining-aware constructors, such as Throwable(Throwable):

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

  1. // Exception with chaining-aware constructor
  2. class HigherLevelException extends Exception {
  3. HigherLevelException(Throwable cause) {
  4. super(cause);
  5. }
  6. }

Most standard exceptions have chaining-aware constructors. For exceptions that don’t, you can set the cause using Throwable’s initCause method. Not only does exception chaining let you access the cause programmatically (with getCause), but it integrates the cause’s stack trace into that of the higher-level exception.

大多数标准异常都有接收链式异常的构造函数。对于不支持链式异常的异常,可以使用 Throwable 的 initCause 方法设置原因。异常链接不仅允许你以编程方式访问原因(使用 getCause),而且还将原因的堆栈跟踪集成到更高层异常的堆栈跟踪中。

While exception translation is superior to mindless propagation of exceptions from lower layers, it should not be overused. Where possible, the best way to deal with exceptions from lower layers is to avoid them, by ensuring that lower-level methods succeed. Sometimes you can do this by checking the validity of the higher-level method’s parameters before passing them on to lower layers.

虽然异常转换优于底层异常的盲目传播,但它不应该被过度使用。在可能的情况下,处理低层异常的最佳方法是确保低层方法避免异常。有时,你可以在将高层方法的参数传递到低层之前检查它们的有效性。

If it is impossible to prevent exceptions from lower layers, the next best thing is to have the higher layer silently work around these exceptions, insulating the caller of the higher-level method from lower-level problems. Under these circumstances, it may be appropriate to log the exception using some appropriate logging facility such as java.util.logging. This allows programmers to investigate the problem, while insulating client code and the users from it.

如果不可能从低层防止异常,那么下一个最好的方法就是让高层静默处理这些异常,使较高层方法的调用者免受低层问题的影响。在这种情况下,可以使用一些适当的日志工具(如 java.util.logging)来记录异常。这允许程序员研究问题,同时将客户端代码和用户与之隔离。

In summary, if it isn’t feasible to prevent or to handle exceptions from lower layers, use exception translation, unless the lower-level method happens to guarantee that all of its exceptions are appropriate to the higher level. Chaining provides the best of both worlds: it allows you to throw an appropriate higherlevel exception, while capturing the underlying cause for failure analysis (Item 75).

总之,如果无法防止或处理来自低层的异常,则使用异常转换,但要保证低层方法的所有异常都适用于较高层。链式异常提供了兼顾两方面的最佳服务:允许抛出适当的高层异常,同时捕获并分析失败的潜在原因(Item-75)。


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

Document all exceptions thrown by each method

A description of the exceptions thrown by a method is an important part of the documentation required to use the method properly. Therefore, it is critically important that you take the time to carefully document all of the exceptions thrown by each method (Item 56).

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

Always declare checked exceptions individually, and document precisely the conditions under which each one is thrown using the Javadoc @throws tag. Don’t take the shortcut of declaring that a method throws some superclass of multiple exception classes that it can throw. As an extreme example, don’t declare that a public method throws Exception or, worse, throws Throwable. In addition to denying any guidance to the method’s user concerning the exceptions it is capable of throwing, such a declaration greatly hinders the use of the method because it effectively obscures any other exception that may be thrown in the same context. One exception to this advice is the main method, which can safely be declared to throw Exception because it is called only by VM.

始终单独声明 checked 异常,并使用 Javadoc 的 @throw 标记精确记录每次抛出异常的条件。如果一个方法抛出多个异常,不要使用快捷方式声明这些异常的超类。作为一个极端的例子,即不要在公共方法声明 throws Exception,或者更糟,甚至 throws Throwable。除了不能向方法的用户提供会抛出哪些异常的任何消息之外,这样的声明还极大地阻碍了方法的使用,因为它掩盖了在相同上下文中可能抛出的任何其他异常。这个建议的一个特例是 main 方法,它可以安全地声明 throw Exception,因为它只被 VM 调用。

While the language does not require programmers to declare the unchecked exceptions that a method is capable of throwing, it is wise to document them as carefully as the checked exceptions. Unchecked exceptions generally represent programming errors (Item 70), and familiarizing programmers with all of the errors they can make helps them avoid making these errors. A well-documented list of the unchecked exceptions that a method can throw effectively describes the preconditions for its successful execution. It is essential that every public method’s documentation describe its preconditions (Item 56), and documenting its unchecked exceptions is the best way to satisfy this requirement.

虽然 Java 不要求程序员为方法声明会抛出的 unchecked 异常,但明智的做法是,应该像 checked 异常一样仔细地记录它们。unchecked 异常通常表示编程错误(Item-70),让程序员熟悉他们可能犯的所有错误可以帮助他们避免犯这些错误。将方法可以抛出的 unchecked 异常形成良好文档,可以有效描述方法成功执行的先决条件。每个公共方法的文档都必须描述它的先决条件(Item-56),记录它的 unchecked 异常是满足这个需求的最佳方法。

It is particularly important that methods in interfaces document the unchecked exceptions they may throw. This documentation forms a part of the interface’s general contract and enables common behavior among multiple implementations of the interface.

特别重要的是,接口中的方法要记录它们可能抛出的 unchecked 异常。此文档构成接口通用约定的一部分,并指明接口的多个实现之间应该遵守的公共行为。

Use the Javadoc @throws tag to document each exception that a method can throw, but do not use the throws keyword on unchecked exceptions. It is important that programmers using your API are aware of which exceptions are checked and which are unchecked because the programmers’ responsibilities differ in these two cases. The documentation generated by the Javadoc @throws tag without a corresponding throws clause in the method declaration provides a strong visual cue to the programmer that an exception is unchecked.

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

It should be noted that documenting all of the unchecked exceptions that each method can throw is an ideal, not always achievable in the real world. When a class undergoes revision, it is not a violation of source or binary compatibility if an exported method is modified to throw additional unchecked exceptions. Suppose a class invokes a method from another, independently written class. The authors of the former class may carefully document all of the unchecked exceptions that each method throws, but if the latter class is revised to throw additional unchecked exceptions, it is quite likely that the former class (which has not undergone revision) will propagate the new unchecked exceptions even though it does not document them.

应该注意的是,记录每个方法会抛出的所有 unchecked 异常是理想的,但在实际中并不总是可以做到。当类进行修订时,如果将导出的方法修改,使其抛出额外的 unchecked 异常,这并不违反源代码或二进制兼容性。假设一个类 A 从另一个独立的类 B 中调用一个方法。A 类的作者可能会仔细记录每个方法抛出的 unchecked 异常,如果 B 类被修改了,使其抛出额外的 unchecked 异常,很可能 A 类(未经修改)将传播新的 unchecked 异常,尽管它没有在文档中声明。

If an exception is thrown by many methods in a class for the same reason, you can document the exception in the class’s documentation comment rather than documenting it individually for each method. A common example is NullPointerException. It is fine for a class’s documentation comment to say, “All methods in this class throw a NullPointerException if a null object reference is passed in any parameter,” or words to that effect.

如果一个类中的许多方法都因为相同的原因抛出异常,你可以在类的文档注释中记录异常, 而不是为每个方法单独记录异常。一个常见的例子是 NullPointerException。类的文档注释可以这样描述:「如果在任何参数中传递了 null 对象引用,该类中的所有方法都会抛出 NullPointerException」,或者类似意思的话。

In summary, document every exception that can be thrown by each method that you write. This is true for unchecked as well as checked exceptions, and for abstract as well as concrete methods. This documentation should take the form of @throws tags in doc comments. Declare each checked exception individually in a method’s throws clause, but do not declare unchecked exceptions. If you fail to document the exceptions that your methods can throw, it will be difficult or impossible for others to make effective use of your classes and interfaces.

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


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

Include failure capture information in detail messages

When a program fails due to an uncaught exception, the system automatically prints out the exception’s stack trace. The stack trace contains the exception’s string representation, the result of invoking its toString method. This typically consists of the exception’s class name followed by its detail message. Frequently this is the only information that programmers or site reliability engineers will have when investigating a software failure. If the failure is not easily reproducible, it may be difficult or impossible to get any more information. Therefore, it is critically important that the exception’s toString method return as much information as possible concerning the cause of the failure. In other words, the detail message of an exception should capture the failure for subsequent analysis.

当程序由于未捕获异常而失败时,系统可以自动打印出异常的堆栈跟踪。堆栈跟踪包含异常的字符串表示,这是调用其 toString 方法的结果。这通常包括异常的类名及其详细信息。通常,这是程序员或管理员在调查软件故障时所掌握的唯一信息。如果失败不容易重现,想获得更多的信息会非常困难。因此,异常的 toString 方法返回尽可能多的关于失败原因的信息是非常重要的。换句话说,由失败导致的异常的详细信息应该被捕获,以便后续分析。

To capture a failure, the detail message of an exception should contain the values of all parameters and fields that contributed to the exception. For example, the detail message of an IndexOutOfBoundsException should contain the lower bound, the upper bound, and the index value that failed to lie between the bounds. This information tells a lot about the failure. Any or all of the three values could be wrong. The index could be one less than the lower bound or equal to the upper bound (a “fencepost error”), or it could be a wild value, far too low or high. The lower bound could be greater than the upper bound (a serious internal invariant failure). Each of these situations points to a different problem, and it greatly aids in the diagnosis if you know what sort of error you’re looking for.

要捕获失败,异常的详细消息应该包含导致异常的所有参数和字段的值。 例如,IndexOutOfBoundsException 的详细消息应该包含下界、上界和未能位于下界之间的索引值。这些信息说明了很多关于失败的信息。这三个值中的任何一个或所有值都可能是错误的。索引可以小于或等于上界(「越界错误」),也可以是一个无效值,太小或太大。下界可能大于上界(严重的内部故障)。每一种情况都指向一个不同的问题,如果你知道你在寻找什么样的错误,这对诊断有很大的帮助。

One caveat concerns security-sensitive information. Because stack traces may be seen by many people in the process of diagnosing and fixing software issues, do not include passwords, encryption keys, and the like in detail messages.

提及一个与安全敏感信息有关的警告。因为许多人在诊断和修复软件问题的过程中可能会看到堆栈跟踪,所以 不应包含密码、加密密钥等详细信息。

While it is critical to include all of the pertinent data in the detail message of an exception, it is generally unimportant to include a lot of prose. The stack trace is intended to be analyzed in conjunction with the documentation and, if necessary, source code. It generally contains the exact file and line number from which the exception was thrown, as well as the files and line numbers of all other method invocations on the stack. Lengthy prose descriptions of the failure are superfluous; the information can be gleaned by reading the documentation and source code.

虽然在异常的详细信息中包含所有相关数据非常重要,但通常不需要包含大量的描述。堆栈跟踪将与文档一起分析,如果需要,还将与源代码一起分析。它通常包含抛出异常的确切文件和行号,以及堆栈上所有方法调用的文件和行号。冗长的描述对一个失败问题而言是多余的;可以通过阅读文档和源代码来收集信息。

The detail message of an exception should not be confused with a user-level error message, which must be intelligible to end users. Unlike a user-level error message, the detail message is primarily for the benefit of programmers or site reliability engineers, when analyzing a failure. Therefore, information content is far more important than readability. User-level error messages are often localized, whereas exception detail messages rarely are. One way to ensure that exceptions contain adequate failure-capture information in their detail messages is to require this information in their constructors instead of a string detail message. The detail message can then be generated automatically to include the information. For example, instead of a String constructor, IndexOutOfBoundsException could have had a constructor that looks like this:

异常的详细信息不应该与用户层的错误消息混淆,因为用户层错误消息最终必须被用户理解。与用户层错误消息不同,详细消息主要是为程序员或管理员在分析故障时提供的。因此,信息内容远比可读性重要。用户层错误消息通常是本地化的,而异常详细信息消息很少本地化。确保异常在其详细信息中包含足够的故障捕获信息的一种方法是,在其构造函数中配置,而不是以传入字符串方式引入这些信息。之后可以自动生成详细信息来包含细节。例如,IndexOutOfBoundsException 构造函数不包含 String 参数,而是像这样:

  1. /**
  2. * Constructs an IndexOutOfBoundsException.
  3. **
  4. @param lowerBound the lowest legal index value
  5. * @param upperBound the highest legal index value plus one
  6. * @param index the actual index value
  7. */
  8. public IndexOutOfBoundsException(int lowerBound, int upperBound, int index) {
  9. // Generate a detail message that captures the failure
  10. super(String.format("Lower bound: %d, Upper bound: %d, Index: %d",lowerBound, upperBound, index));
  11. // Save failure information for programmatic access
  12. this.lowerBound = lowerBound;
  13. this.upperBound = upperBound;
  14. this.index = index;
  15. }

As of Java 9, IndexOutOfBoundsException finally acquired a constructor that takes an int valued index parameter, but sadly it omits the lowerBound and upperBound parameters. More generally, the Java libraries don’t make heavy use of this idiom, but it is highly recommended. It makes it easy for the programmer throwing an exception to capture the failure. In fact, it makes it hard for the programmer not to capture the failure! In effect, the idiom centralizes the code to generate a high-quality detail message in the exception class, rather than requiring each user of the class to generate the detail message redundantly.

从 Java 9 开始,IndexOutOfBoundsException 最终获得了一个接受 int 值索引参数的构造函数,但遗憾的是它忽略了下界和上界参数。更一般地说,Java 库不会大量使用这个习惯用法,但是强烈推荐使用它。它使程序员很容易通过抛出异常来捕获失败。事实上,它使程序员不想捕获失败都难!实际上,这个习惯用法将集中在异常类中生成高质量的详细信息,而不是要求该类的每个用户都生成冗余的详细信息。

译注:IndexOutOfBoundsException 有关 int 参数的构造函数源码

  1. /**
  2. * Constructs a new {@code IndexOutOfBoundsException} class with an
  3. * argument indicating the illegal index.
  4. *
  5. * <p>The index is included in this exception's detail message. The
  6. * exact presentation format of the detail message is unspecified.
  7. *
  8. * @param index the illegal index.
  9. * @since 9
  10. */
  11. public IndexOutOfBoundsException(int index) {
  12. super("Index out of range: " + index);
  13. }

As suggested in Item 70, it may be appropriate for an exception to provide accessor methods for its failure-capture information (lowerBound, upperBound, and index in the above example). It is more important to provide such accessor methods on checked exceptions than unchecked, because the failure-capture information could be useful in recovering from the failure. It is rare (although not inconceivable) that a programmer might want programmatic access to the details of an unchecked exception. Even for unchecked exceptions, however, it seems advisable to provide these accessors on general principle (Item 12, page 57).

正如 Item-70 中建议的,异常为其故障捕获信息提供访问器方法是适合的(上面示例中的下界、上界和索引)。在 checked 异常上提供此类访问器方法比 unchecked 异常上提供此类访问器方法更为重要,因为故障捕获信息可能有助于程序从故障中恢复。程序员可能希望通过编程访问 unchecked 异常的详细信息,但这是很少见的(尽管是可以想象的)。然而,即使对于 unchecked 异常,根据一般原则,提供这些访问器也是可以的(Item-12,第 57 页)。


【76】尽力保证故障原子性

Strive for failure atomicity

After an object throws an exception, it is generally desirable that the object still be in a well-defined, usable state, even if the failure occurred in the midst of performing an operation. This is especially true for checked exceptions, from which the caller is expected to recover. Generally speaking, a failed method invocation should leave the object in the state that it was in prior to the invocation. A method with this property is said to be failure-atomic.

在对象抛出异常之后,通常希望对象仍然处于定义良好的可用状态,即使在执行操作时发生了故障。对于 checked 异常尤其如此,调用者希望从异常中恢复。一般来说,失败的方法调用应该使对象处于调用之前的状态。 具有此属性的方法称为具备故障原子性。

There are several ways to achieve this effect. The simplest is to design immutable objects (Item 17). If an object is immutable, failure atomicity is free. If an operation fails, it may prevent a new object from getting created, but it will never leave an existing object in an inconsistent state, because the state of each object is consistent when it is created and can’t be modified thereafter.

有几种方式可以达到这种效果。最简单的方法是设计不可变对象(Item-17)。如果对象是不可变的,则故障原子性是必然的。如果一个操作失败,它可能会阻止创建一个新对象,但是它不会让一个现有对象处于不一致的状态,因为每个对象的状态在创建时是一致的,并且在创建后不能修改。

For methods that operate on mutable objects, the most common way to achieve failure atomicity is to check parameters for validity before performing the operation (Item 49). This causes most exceptions to get thrown before object modification commences. For example, consider the Stack.pop method in Item 7:

对于操作可变对象的方法,实现故障原子性的最常见方法是在执行操作之前检查参数的有效性(Item-49)。这使得大多数异常在对象修改开始之前被抛出。例如,考虑 Stack.pop 方法(Item-7):

  1. public Object pop() {
  2. if (size == 0)
  3. throw new EmptyStackException();
  4. Object result = elements[--size];
  5. elements[size] = null; // Eliminate obsolete reference
  6. return result;
  7. }

If the initial size check were eliminated, the method would still throw an exception when it attempted to pop an element from an empty stack. It would, however, leave the size field in an inconsistent (negative) state, causing any future method invocations on the object to fail. Additionally, the ArrayIndexOutOfBoundsException thrown by the pop method would be inappropriate to the abstraction (Item 73).

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

A closely related approach to achieving failure atomicity is to order the computation so that any part that may fail takes place before any part that modifies the object. This approach is a natural extension of the previous one when arguments cannot be checked without performing a part of the computation. For example, consider the case of TreeMap, whose elements are sorted according to some ordering. In order to add an element to a TreeMap, the element must be of a type that can be compared using the TreeMap’s ordering. Attempting to add an incorrectly typed element will naturally fail with a ClassCastException as a result of searching for the element in the tree, before the tree has been modified in any way.

实现故障原子性的另一种方式是对计算进行排序,以便可能发生故障的部分都先于修改对象的部分发生。当执行某部分计算才能检查参数时,这种方法是前一种方法的自然扩展。例如,考虑 TreeMap 的情况,它的元素按照一定的顺序排序。为了向 TreeMap 中添加元素,元素的类型必须能够使用 TreeMap 的顺序进行比较。在以任何方式修改「树」之前,由于在「树」中搜索元素,试图添加类型不正确的元素自然会失败,并导致 ClassCastException 异常。

A third approach to achieving failure atomicity is to perform the operation on a temporary copy of the object and to replace the contents of the object with the temporary copy once the operation is complete. This approach occurs naturally when the computation can be performed more quickly once the data has been stored in a temporary data structure. For example, some sorting functions copy their input list into an array prior to sorting to reduce the cost of accessing elements in the inner loop of the sort. This is done for performance, but as an added benefit, it ensures that the input list will be untouched if the sort fails.

实现故障原子性的第三种方法是以对象的临时副本执行操作,并在操作完成后用临时副本替换对象的内容。当数据存储在临时数据结构中后,计算过程会更加迅速,这种办法就是很自然的。例如,一些排序函数在排序之前将其输入 list 复制到数组中,以降低访问排序内循环中的元素的成本。这样做是为了提高性能,但是作为一个额外的好处,它确保如果排序失败,输入 list 将保持不变。

A last and far less common approach to achieving failure atomicity is to write recovery code that intercepts a failure that occurs in the midst of an operation, and causes the object to roll back its state to the point before the operation began. This approach is used mainly for durable (disk-based) data structures.

实现故障原子性的最后一种不太常见的方法是编写恢复代码,拦截在操作过程中发生的故障,并使对象回滚到操作开始之前的状态。这种方法主要用于持久的(基于磁盘的)数据结构。

While failure atomicity is generally desirable, it is not always achievable. For example, if two threads attempt to modify the same object concurrently without proper synchronization, the object may be left in an inconsistent state. It would therefore be wrong to assume that an object was still usable after catching a ConcurrentModificationException. Errors are unrecoverable, so you need not even attempt to preserve failure atomicity when throwing AssertionError.

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

Even where failure atomicity is possible, it is not always desirable. For some operations, it would significantly increase the cost or complexity. That said, it is often both free and easy to achieve failure atomicity once you’re aware of the issue.

即使在可以实现故障原子性的情况下,也并不总是可取的。对于某些操作,它将显著增加成本或复杂性。也就是说,一旦意识到这个问题,就可以轻松地实现故障原子性。

In summary, as a rule, any generated exception that is part of a method’s specification should leave the object in the same state it was in prior to the method invocation. Where this rule is violated, the API documentation should clearly indicate what state the object will be left in. Unfortunately, plenty of existing API documentation fails to live up to this ideal.

总之,作为规则,也作为方法规范的一部分,生成的任何异常都应该使对象保持在方法调用之前的状态。如果违反了这条规则,API 文档应该清楚地指出对象将处于什么状态。不幸的是,许多现有的 API 文档都没有做到。


【77】不要忽略异常

Don’t ignore exceptions

While this advice may seem obvious, it is violated often enough that it bears repeating. When the designers of an API declare a method to throw an exception, they are trying to tell you something. Don’t ignore it! It is easy to ignore exceptions by surrounding a method invocation with a try statement whose catch block is empty:

虽然这一建议似乎显而易见,但它经常被违反,因此值得强调。当 API 的设计人员声明一个抛出异常的方法时,他们试图告诉你一些事情。不要忽略它!如果在方法调用的周围加上一条 try 语句,其 catch 块为空,可以很容易忽略异常:

  1. // Empty catch block ignores exception - Highly suspect!
  2. try {
  3. ...
  4. }
  5. catch (SomeException e) {
  6. }

An empty catch block defeats the purpose of exceptions, which is to force you to handle exceptional conditions. Ignoring an exception is analogous to ignoring a fire alarm—and turning it off so no one else gets a chance to see if there’s a real fire. You may get away with it, or the results may be disastrous. Whenever you see an empty catch block, alarm bells should go off in your head.

空 catch 块违背了异常的目的, 它的存在是为了强制你处理异常情况。忽略异常类似于忽略火灾警报一样,关掉它之后,其他人就没有机会看到是否真的发生了火灾。你可能侥幸逃脱,但结果可能是灾难性的。每当你看到一个空的 catch 块,你的脑海中应该响起警报。

There are situations where it is appropriate to ignore an exception. For example, it might be appropriate when closing a FileInputStream. You haven’t changed the state of the file, so there’s no need to perform any recovery action, and you’ve already read the information that you need from the file, so there’s no reason to abort the operation in progress. It may be wise to log the exception, so that you can investigate the matter if these exceptions happen often. If you choose to ignore an exception, the catch block should contain a comment explaining why it is appropriate to do so, and the variable should be named ignored:

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

  1. Future<Integer> f = exec.submit(planarMap::chromaticNumber);
  2. int numColors = 4; // Default; guaranteed sufficient for any map
  3. try {
  4. numColors = f.get(1L, TimeUnit.SECONDS);
  5. }
  6. catch (TimeoutException | ExecutionException ignored) {
  7. // Use default: minimal coloring is desirable, not required
  8. }

The advice in this item applies equally to checked and unchecked exceptions. Whether an exception represents a predictable exceptional condition or a programming error, ignoring it with an empty catch block will result in a program that continues silently in the face of error. The program might then fail at an arbitrary time in the future, at a point in the code that bears no apparent relation to the source of the problem. Properly handling an exception can avert failure entirely. Merely letting an exception propagate outward can at least cause the program to fail swiftly, preserving information to aid in debugging the failure.

本条目中的建议同样适用于 checked 异常和 unchecked 异常。不管异常是表示可预测的异常条件还是编程错误,用空 catch 块忽略它将导致程序在错误面前保持静默。然后,程序可能会在未来的任意时间点,在与问题源没有明显关系的代码中失败。正确处理异常可以完全避免失败。仅仅让异常向外传播,可能会导致程序走向失败,保留信息有利于调试。