mirror of
https://github.com/sjsdfg/effective-java-3rd-chinese.git
synced 2025-01-26 20:30:36 +08:00
更新标点符号
This commit is contained in:
parent
e5eb927b0a
commit
15c639700d
@ -1,6 +1,6 @@
|
||||
# 63. 当心字符串连接引起的性能问题
|
||||
|
||||
字符串连接操作符 `(+)` 是将几个字符串组合成一个字符串的简便方法。对于生成单行输出或构造一个小的、固定大小的对象的字符串表示形式,它是可以的,但是它不能伸缩。使用 **字符串串联运算符重复串联 n 个字符串需要 n 的平方级时间。** 这是字符串不可变这一事实导致的结果([Item-17](https://github.com/clxering/Effective-Java-3rd-edition-Chinese-English-bilingual/blob/master/Chapter-4/Chapter-4-Item-17-Minimize-mutability.md))。当连接两个字符串时,将复制这两个字符串的内容。
|
||||
字符串连接操作符 `(+)` 是将几个字符串组合成一个字符串的简便方法。对于生成单行输出或构造一个小的、固定大小的对象的字符串表示形式,它是可以的,但是它不能伸缩。使用 **字符串串联运算符重复串联 n 个字符串需要 n 的平方级时间。** 这是字符串不可变这一事实导致的结果(详见第 17 条)。当连接两个字符串时,将复制这两个字符串的内容。
|
||||
|
||||
例如,考虑这个方法,它通过将每个账单项目重复连接到一行来构造账单语句的字符串表示:
|
||||
|
||||
|
@ -2,21 +2,21 @@
|
||||
|
||||
条目 51 指出,应该使用接口而不是类作为参数类型。更一般地说,你应该优先使用接口而不是类来引用对象。**如果存在合适的接口类型,那么应该使用接口类型声明参数、返回值、变量和字段。** 惟一真正需要引用对象的类的时候是使用构造函数创建它的时候。为了具体说明这一点,考虑 LinkedHashSet 的情况,它是 Set 接口的一个实现。声明时应养成这样的习惯:
|
||||
|
||||
```
|
||||
```java
|
||||
// Good - uses interface as type
|
||||
Set<Son> sonSet = new LinkedHashSet<>();
|
||||
```
|
||||
|
||||
而不是这样:
|
||||
|
||||
```
|
||||
```java
|
||||
// Bad - uses class as type!
|
||||
LinkedHashSet<Son> sonSet = new LinkedHashSet<>();
|
||||
```
|
||||
|
||||
**如果你养成了使用接口作为类型的习惯,那么你的程序将更加灵活。** 如果你决定要切换实现,只需在构造函数中更改类名(或使用不同的静态工厂)。例如,第一个声明可以改为:
|
||||
|
||||
```
|
||||
```java
|
||||
Set<Son> sonSet = new HashSet<>();
|
||||
```
|
||||
|
||||
|
@ -60,11 +60,11 @@ private static void fatalError(String msg) {
|
||||
}
|
||||
```
|
||||
|
||||
虽然这个程序只是一个小把戏,但它演示的技术非常强大。这个程序可以很容易地转换成一个通用的集合测试器,通过积极地操作一个或多个实例并检查它们是否遵守 Set 接口约定来验证指定的 Set 实现。类似地,它可以变成一个通用的集合性能分析工具。事实上,该技术足够强大,可以实现一个成熟的服务提供者框架(详见第 1 条)。
|
||||
虽然这个程序只是一个小把戏,但它演示的技术非常强大。这个程序可以很容易地转换成一个通用的集合测试器,通过积极地操作一个或多个实例并检查它们是否遵守 `Set` 接口约定来验证指定的 `Set` 实现。类似地,它可以变成一个通用的集合性能分析工具。事实上,该技术足够强大,可以实现一个成熟的服务提供者框架(详见第 1 条)。
|
||||
|
||||
这个例子也说明了反射的两个缺点。首先,该示例可以在运行时生成六个不同的异常,如果没有使用反射实例化,所有这些异常都将是编译时错误。(有趣的是,你可以通过传入适当的命令行参数,使程序生成六个异常中的每一个。)第二个缺点是,根据类的名称生成类的实例需要 25 行冗长的代码,而构造函数调用只需要一行。通过捕获 ReflectiveOperationException(Java 7 中引入的各种反射异常的超类),可以减少程序的长度。这两个缺点都只限于实例化对象的程序部分。实例化后,与任何其他 Set 实例将难以区分。在实际的程序中,通过这种限定使用反射的方法,大部分代码可以免受影响。
|
||||
这个例子也说明了反射的两个缺点。首先,该示例可以在运行时生成六个不同的异常,如果没有使用反射实例化,所有这些异常都将是编译时错误。(有趣的是,你可以通过传入适当的命令行参数,使程序生成六个异常中的每一个。)第二个缺点是,根据类的名称生成类的实例需要 25 行冗长的代码,而构造函数调用只需要一行。通过捕获 `ReflectiveOperationException`(Java 7 中引入的各种反射异常的超类),可以减少程序的长度。这两个缺点都只限于实例化对象的程序部分。实例化后,与任何其他 Set 实例将难以区分。在实际的程序中,通过这种限定使用反射的方法,大部分代码可以免受影响。
|
||||
|
||||
如果编译此程序,将得到 unchecked 的强制转换警告。这个警告是合法的,即使指定的类不是 Set 实现,`Class<? extends Set<String>>` 也会成功,在这种情况下,程序在实例化类时抛出 ClassCastException。要了解如何抑制警告,请阅读条目 27。
|
||||
如果编译此程序,将得到 unchecked 的强制转换警告。这个警告是合法的,即使指定的类不是 `Set` 实现,`Class<? extends Set<String>>` 也会成功,在这种情况下,程序在实例化类时抛出 `ClassCastException`。要了解如何抑制警告,请阅读条目 27。
|
||||
|
||||
反射的合法用途(很少)是管理类对运行时可能不存在的其他类、方法或字段的依赖关系。如果你正在编写一个包,并且必须针对其他包的多个版本运行,此时反射将非常有用。该技术是根据支持包所需的最小环境(通常是最老的版本)编译包,并反射性地访问任何较新的类或方法。如果你试图访问的新类或方法在运行时不存在,要使此工作正常进行,则必须采取适当的操作。适当的操作可能包括使用一些替代方法来完成相同的目标,或者使用简化的功能进行操作。
|
||||
|
||||
|
@ -4,15 +4,15 @@
|
||||
|
||||
重用标准的常有多个好处。其中最主要的好处是,它使 API 更易于学习和使用,因为它与程序员已经熟悉的习惯用法一致。第二个好处是,对于用到这些 API 程序而言,它们的可读性会更好,因为它们不会出现很多程序员不熟悉的异常。最后(也是最不重要的)一点是,异常类越少,意味着内存占用(footprint)就越小,装载这些类的时间开销也越少。
|
||||
|
||||
最经常被重用的异常类型是 IllegalArgumentException(详见第 49 条)。当调用者传递的参数值不合适的时候,往往就会抛出这个异常。比如,假设某一个参数代表了“某个动作的重复次数”,如果程序员给这个参数传递了一个负数,就会抛出这个异常。
|
||||
最经常被重用的异常类型是 `IllegalArgumentException`(详见第 49 条)。当调用者传递的参数值不合适的时候,往往就会抛出这个异常。比如,假设某一个参数代表了“某个动作的重复次数”,如果程序员给这个参数传递了一个负数,就会抛出这个异常。
|
||||
|
||||
另一个经常被重用的异常是 llegalStateException。如果因为接收对象的状态而使调用非法,通常就会抛出这个异常。例如,如果在某个对象被正确地初始化之前,调用者就企图使用这个对象,就会抛出这个异常。
|
||||
另一个经常被重用的异常是 `llegalStateException`。如果因为接收对象的状态而使调用非法,通常就会抛出这个异常。例如,如果在某个对象被正确地初始化之前,调用者就企图使用这个对象,就会抛出这个异常。
|
||||
|
||||
可以这么说,所有错误的方法调用都可以被归结为非法参数或者非法状态,但是,还有一些其他的标准异常也被用于某些特定情况下的非法参数和非法状态。如果调用者在某个不允许 null 值的参数中传递了 null,习惯的做法就是抛出 NullPointerException 异常,而不是 IllegalArgumentException。同样地,如果调用者在表示序列下标的参数中传递了越界的值,应该抛出的就是 IndexOutOfBoundsException 异常,而不是 IllegalArgumentException。
|
||||
可以这么说,所有错误的方法调用都可以被归结为非法参数或者非法状态,但是,还有一些其他的标准异常也被用于某些特定情况下的非法参数和非法状态。如果调用者在某个不允许 null 值的参数中传递了 null,习惯的做法就是抛出 `NullPointerException` 异常,而不是 `IllegalArgumentException`。同样地,如果调用者在表示序列下标的参数中传递了越界的值,应该抛出的就是 `IndexOutOfBoundsException` 异常,而不是 `IllegalArgumentException`。
|
||||
|
||||
另一个值得了解的通用异常是 ConcurrentModificationException。如果检测到一个专门设计用于单线程的对象,或者与外部同步机制配合使用的对象正在(或已经)被并发地修改,就应该抛出这个异常。这个异常顶多就是一个提示,因为不可能可靠地侦测到并发的修改。
|
||||
另一个值得了解的通用异常是 `ConcurrentModificationException`。如果检测到一个专门设计用于单线程的对象,或者与外部同步机制配合使用的对象正在(或已经)被并发地修改,就应该抛出这个异常。这个异常顶多就是一个提示,因为不可能可靠地侦测到并发的修改。
|
||||
|
||||
最后一个值得注意的标准异常是 UnsupportedOperationException。如果对象不支持所请求的操作,就会抛出这个异常。很少用到它,因为绝大多数对象都会支持它们实现的所有方法。如果类没有实现由它们实现的接口所定义的一个或者多个可选操作(optional operation),它就可以使用这个异常。例如,对于只支持追加操作的 List 实现,如果有人试图从列表中删除元素,它就会抛出这个异常。
|
||||
最后一个值得注意的标准异常是 `UnsupportedOperationException`。如果对象不支持所请求的操作,就会抛出这个异常。很少用到它,因为绝大多数对象都会支持它们实现的所有方法。如果类没有实现由它们实现的接口所定义的一个或者多个可选操作(optional operation),它就可以使用这个异常。例如,对于只支持追加操作的 List 实现,如果有人试图从列表中删除元素,它就会抛出这个异常。
|
||||
|
||||
**不要直接重用 Exception、RuntimeException、Throwable 或者 Error。** 对待这些类要像对待抽象类一样。你无法可靠地测试这些异常,因为它们是一个方法可能抛出的其他异常的超类。
|
||||
|
||||
@ -28,6 +28,7 @@
|
||||
| ConcurrentModificationException | 在禁止并发修改的情况下,检测到对象的并发修改 |
|
||||
| UnsupportedOperationException | 对象不支持用户请求的方法 |
|
||||
|
||||
然这些都是 Java 平台类库中迄今为止最常被重用的异常,但是,在条件许可的情况下,其他的异常也可以被重用。例如,如果要实现诸如复数或者有理数之类的算术对象,也可以重用 ArithmeticException 和 NumberFormatException。如果某个异常能够满足你的需要,就不要犹豫,使用就是,不过一定要确保抛出异常的条件与该异常的文档中描述的条件一致。这种重用必须建立在语义的基础上,而不是建立在名称的基础之上。而且,如果希望稍微增加更多的失败一捕获(failure-capture)信息(详见第 75 条),可以放心地子类化标准异常,但要记住异常是可序列化的(详见第 12 章)。这也正是“如果没有非常正当的理由,千万不要自己编写异常类”的原因。
|
||||
然这些都是 Java 平台类库中迄今为止最常被重用的异常,但是,在条件许可的情况下,其他的异常也可以被重用。例如,如果要实现诸如复数或者有理数之类的算术对象,也可以重用 `ArithmeticException` 和 `NumberFormatException`。如果某个异常能够满足你的需要,就不要犹豫,使用就是,不过一定要确保抛出异常的条件与该异常的文档中描述的条件一致。这种重用必须建立在语义的基础上,而不是建立在名称的基础之上。而且,如果希望稍微增加更多的失败一捕获(failure-capture)信息(详见第 75 条),可以放心地子类化标准异常,但要记住异常是可序列化的(详见第 12 章)。这也正是“如果没有非常正当的理由,千万不要自己编写异常类”的原因。
|
||||
|
||||
选择重用哪一种异常并非总是那么精确,因为上表中的“使用场合”并不是相互排斥的比如,以表示一副纸牌的对象为例。假设有一个处理发牌操作的方法,它的参数是发一手牌的纸牌张数。假设调用者在这个参数中传递的值大于整副纸牌的剩余张数。这种情形既可以被解释为 `IllegalArgumentException`( handSize 参数的值太大),也可以被解释为 `IllegalStateException`(纸牌对象包含的纸牌太少)。在这种情况下,**如果没有可用的参数值,就抛出 `IllegalStateException`,否则就抛出 `IllegalArgumentException`。**
|
||||
|
||||
选择重用哪一种异常并非总是那么精确,因为上表中的“使用场合”并不是相互排斥的比如,以表示一副纸牌的对象为例。假设有一个处理发牌操作的方法,它的参数是发一手牌的纸牌张数。假设调用者在这个参数中传递的值大于整副纸牌的剩余张数。这种情形既可以被解释为 IllegalArgumentException( handSize 参数的值太大),也可以被解释为 IllegalStateException(纸牌对象包含的纸牌太少)。在这种情况下,**如果没有可用的参数值,就抛出 llegalStateException,否则就抛出工 llegalArgumentException。**
|
@ -2,7 +2,7 @@
|
||||
|
||||
如果方法抛出的异常与它所执行的任务没有明显的联系,这种情形将会使人不知所措。当方法传递由低层抽象抛出的异常时,往往会发生这种情况。除了使人感到困惑之外,这也“污染”了具有实现细节的更高层的 API 。如果高层的实现在后续的发行版本中发生了变化,它所抛出的异常也可能会跟着发生变化,从而潜在地破坏现有的客户端程序。
|
||||
|
||||
为了避免这个问题, **更高层的实现应该捕获低层的异常,同时抛出可以按照高层抽象进行解释的异常。**这种做法称为异常转译 (exception translation),如下代码所示:
|
||||
为了避免这个问题, **更高层的实现应该捕获低层的异常,同时抛出可以按照高层抽象进行解释的异常。**这种做法称为异常转译 (exception translation),如下代码所示:
|
||||
|
||||
```java
|
||||
/* Exception Translation */
|
||||
@ -13,7 +13,7 @@ try {
|
||||
}
|
||||
```
|
||||
|
||||
下面的异常转译例子取自于 AbstractSequentialList 类,该类是 List 接口的一个骨架实现 (skeletal implementation),详见第 20 条。在这个例子中,按照`List<E>`接口中 get 方法的规范要求,异常转译是必需的:
|
||||
下面的异常转译例子取自于 `AbstractSequentialList` 类,该类是 List 接口的一个骨架实现 (skeletal implementation),详见第 20 条。在这个例子中,按照`List<E>`接口中 get 方法的规范要求,异常转译是必需的:
|
||||
|
||||
```java
|
||||
/**
|
||||
@ -31,7 +31,7 @@ public E get( int index ) {
|
||||
}
|
||||
```
|
||||
|
||||
一种特殊的异常转译形式称为异常链 (exception chaining),如果低层的异常对于调试导致高层异常的问题非常有帮助,使用异常链就很合适。低层的异常(原因)被传到高层的异常,高层的异常提供访问方法 (Throwable 的 getCause 方法)来获得低层的异常:
|
||||
一种特殊的异常转译形式称为异常链(exception chaining),如果低层的异常对于调试导致高层异常的问题非常有帮助,使用异常链就很合适。低层的异常(原因)被传到高层的异常,高层的异常提供访问方法 (Throwable 的 getCause 方法)来获得低层的异常:
|
||||
|
||||
```java
|
||||
// Exception Chaining
|
||||
@ -53,10 +53,10 @@ class HigherLevelException extends Exception {
|
||||
}
|
||||
```
|
||||
|
||||
大多数标准的异常都有支持链的构造器。对于没有支持链的异常,可以利用 Throwable 的 initCause 方法设置原因。异常链不仅让你可以通过程序(用 getCause) 访问原因,还可以将原因的堆战轨迹集成到更高层的异常中。
|
||||
大多数标准的异常都有支持链的构造器。对于没有支持链的异常,可以利用 Throwable 的 initCause 方法设置原因。异常链不仅让你可以通过程序(用 getCause)访问原因,还可以将原因的堆战轨迹集成到更高层的异常中。
|
||||
|
||||
**尽管异常转译与不加选择地从低层传递异常的做法相比有所改进,但是也不能滥用它。** 如有可能,处理来自低层异常的最好做法是,在调用低层方法之前确保它们会成功执行,从而避免它们抛出异常。有时候,可以在给低层传递参数之前,检查更高层方法的参数的有效性,从而避免低层方法抛出异常。
|
||||
|
||||
如果无法阻止来自低层的异常,其次的做法是,让更高层来悄悄地处理这些异常,从而将高层方法的调用者与低层的问题隔离开来。在这种情况下,可以用某种适当的记录机制(如 java.util.logging) 将异常记录下来。这样有助于管理员调查问题,同时又将客户端代码和最终用户与问题隔离开来。
|
||||
如果无法阻止来自低层的异常,其次的做法是,让更高层来悄悄地处理这些异常,从而将高层方法的调用者与低层的问题隔离开来。在这种情况下,可以用某种适当的记录机制(如 java.util.logging)将异常记录下来。这样有助于管理员调查问题,同时又将客户端代码和最终用户与问题隔离开来。
|
||||
|
||||
总而言之,如果不能阻止或者处理来自更低层的异常,一般的做法是使用异常转译,只有在低层方法的规范碰巧可以保证“它所抛出的所有异常对于更高层也是合适的”情况下,才可以将异常从低层传播到高层。异常链对高层和低层异常都提供了最佳的功能:它允许抛出适当的高层异常,同时又能捕获低层的原因进行失败分析(详见第 75 条) 。
|
@ -2,16 +2,16 @@
|
||||
|
||||
描述一个方法所抛出的异常,是正确使用这个方法时所需文档的重要组成部分。因此,花点时间仔细地为每个方法抛出的异常建立文档是特别重要的。
|
||||
|
||||
**始终要单独地声明受检异常,** 并且利用 Javadoc 的@ throws 标签, **准确地记录下抛出每个异常的条件。** 如果一个公有方法可能抛出多个异常类,则不要使用“快捷方式”声明它会抛出这些异常类的某个超类。永远不要声明一个公有方法直接“ throws Exception”,或者更糟糕的是声明它直接“ throws Throwable ”,这是非常极端的例子。这样的声明不仅没有为程序员提供关于“这个方法能够抛出哪些异常”的任何指导信息,而且大大地妨碍了该方法的使用,因为它实际上掩盖了该方法在同样的执行环境下可能抛出的任何其他异常。这条建议有一个例外,就是 main 方法它可以被安全地声明抛出 Exception ,因为它只通过虚拟机调用。
|
||||
**始终要单独地声明受检异常,** 并且利用 Javadoc 的 @throws 标签, **准确地记录下抛出每个异常的条件。** 如果一个公有方法可能抛出多个异常类,则不要使用“快捷方式”声明它会抛出这些异常类的某个超类。永远不要声明一个公有方法直接“ throws Exception”,或者更糟糕的是声明它直接“ throws Throwable ”,这是非常极端的例子。这样的声明不仅没有为程序员提供关于“这个方法能够抛出哪些异常”的任何指导信息,而且大大地妨碍了该方法的使用,因为它实际上掩盖了该方法在同样的执行环境下可能抛出的任何其他异常。这条建议有一个例外,就是 main 方法它可以被安全地声明抛出 Exception ,因为它只通过虚拟机调用。
|
||||
|
||||
虽然 Java 语言本身并没有要求程序员为一个方法声明它可能会抛出的未受检异常,但是,如同受检异常一样,仔细地为它们建立文档是非常明智的。未受检异常通常代表编程上的错误(详见第 70 条),让程序员了解所有这些错误都有助于帮助他们避免犯同样的错误。对于方法可能抛出的未受检异常,如果将这些异常信息很好地组织成列表文档,就可以有效地描述出这个方法被成功执行的前提条件。每个方法的文档应该描述它的前提条件(详见第 56 条),这是很重要的,在文档中记录下未受检异常是满足前提条件的最佳做法。
|
||||
虽然 Java 语言本身并没有要求程序员为一个方法声明它可能会抛出的未受检异常,但是,如同受检异常一样,仔细地为它们建立文档是非常明智的。未受检异常通常代表编程上的错误(详见第 70 条),让程序员了解所有这些错误都有助于帮助他们避免犯同样的错误。对于方法可能抛出的未受检异常,如果将这些异常信息很好地组织成列表文档,就可以有效地描述出这个方法被成功执行的前提条件。每个方法的文档应该描述它的前提条件(详见第 56 条),这是很重要的,在文档中记录下未受检异常是满足前提条件的最佳做法。
|
||||
|
||||
对于接口中的方法,在文档中记录下它可能抛出的未受检异常显得尤为重要。这份文档构成了该接口的通用约定( general contract )的一部分,它指定了该接口的多个实现必须遵循的公共行为。
|
||||
对于接口中的方法,在文档中记录下它可能抛出的未受检异常显得尤为重要。这份文档构成了该接口的通用约定(general contract )的一部分,它指定了该接口的多个实现必须遵循的公共行为。
|
||||
|
||||
**使用 Javadoc 的 @throws 标签记录下一个方法可能抛出的每个未受检异常,但是不要使用 throws 关键字将未受检的异常包含在方法的声明中。** 使用 API 的程序员必须知道哪些异常是需要受检的,哪些是不需要受检的,因为他们有责任区分这两种情形。当缺少由 throws 声明产生的方法标头时,由 Javadoc 的 @throws 标签所产生的文档就会提供明显的提示信息,以帮助程序员区分受检异常和未受检异常。
|
||||
|
||||
应该注意的是,为每个方法可能抛出的所有未受检异常建立文档是很理想的,但是在实践中并非总能做到这一点。当类被修订之后,如果有个导出方法被修改了,它将会抛出额外的未受检异常,这不算违反源代码或者二进制兼容性。假设一个类调用了另一个独立编写的类中的方法。第一个类的编写者可能会为每个方法抛出的未受检异常仔细地建立文档,但是,如果第二个类被修订了,抛出了额外的未受检异常,很有可能第一个类(它并没有被修订)就会把新的未受检异常传播出去,尽管它并没有声明这些异常。
|
||||
|
||||
**如果一个类中的许多方法出于同样的原因而抛出同一个异常,在该类的文档注释中对这个异常建立文档,这是可以接受的,** 而不是为每个方法单独建立文档。一个常见的例子是 NullPointerException。 若类的文档注释中有这样的描述:"All methods in this class throw aNullPointerException if a null object reference is passed in any parameter ”(如果 null 对象引用被传递到任何一个参数中,这个类中的所有方法都会抛出 NullPointerException ),或者有其他类似的语句,这是可以的。
|
||||
**如果一个类中的许多方法出于同样的原因而抛出同一个异常,在该类的文档注释中对这个异常建立文档,这是可以接受的,** 而不是为每个方法单独建立文档。一个常见的例子是 NullPointerException。 若类的文档注释中有这样的描述:"All methods in this class throw a NullPointerException if a null object reference is passed in any parameter ”(如果 null 对象引用被传递到任何一个参数中,这个类中的所有方法都会抛出 NullPointerException),或者有其他类似的语句,这是可以的。
|
||||
|
||||
总而言之,要为你编写的每个方法所能抛出的每个异常建立文档。对于未受检异常和受检异常,以及抽象的方法和具体的方法一概如此。这个文档在文档注释中应当采用@throws 标签的形式。要在方法的 throws 子句中为每个受检异常提供单独的声明,但是不要声明未受检的异常。如果没有为可以抛出的异常建立文档,其他人就很难或者根本不可能有效地使用你的类和接口。
|
||||
总而言之,要为你编写的每个方法所能抛出的每个异常建立文档。对于未受检异常和受检异常,以及抽象的方法和具体的方法一概如此。这个文档在文档注释中应当采用 @throws 标签的形式。要在方法的 throws 子句中为每个受检异常提供单独的声明,但是不要声明未受检的异常。如果没有为可以抛出的异常建立文档,其他人就很难或者根本不可能有效地使用你的类和接口。
|
@ -1,8 +1,8 @@
|
||||
# 75. 在细节消息中包含失败一捕获信息
|
||||
|
||||
当程序由于未被捕获的异常而失败的时’候,系统会自动地打印出该异常的堆栈轨迹。在堆栈轨迹中包含该异常的字符串表示法 (string representation),即它的 toString 方法的调用结果。它通常包含该异常的类名,紧随其后的是细节消息 (detail message)。通常,这只是程序员或者网站可靠性工程师在调查软件失败原因时必须检查的信息。如果失败的情形不容易重现,要想获得更多的信息会非常困难,甚至是不可能的。因此,异常类型的 toString 方法应该尽可能多地返回有关失败原因的信息,这一点特别重要。换句话说,异常的字符串表示法应该捕获失败,以便于后续进行分析。
|
||||
当程序由于未被捕获的异常而失败的时’候,系统会自动地打印出该异常的堆栈轨迹。在堆栈轨迹中包含该异常的字符串表示法 (string representation),即它的 toString 方法的调用结果。它通常包含该异常的类名,紧随其后的是细节消息 (detail message)。通常,这只是程序员或者网站可靠性工程师在调查软件失败原因时必须检查的信息。如果失败的情形不容易重现,要想获得更多的信息会非常困难,甚至是不可能的。因此,异常类型的 toString 方法应该尽可能多地返回有关失败原因的信息,这一点特别重要。换句话说,异常的字符串表示法应该捕获失败,以便于后续进行分析。
|
||||
|
||||
**为了捕获失败,异常的细节信息应该包含“对该异常有贡献”的所有参数和字段的值。** 例如, IndexOutOfBoundsException 异常的细节消息应该包含下界、上界以及没有落在界内的下标值。该细节消息提供了许多关于失败的信息。这三个值中任何一个或者全部都有可能是错的。实际的下标值可能小于下界或等于上界(“越界错误”),或者它可能是个无效值,太小或太大。下界也有可能大于上界(严重违反内部约束条件的一种情况) 。每一种情形都代表了不同的问题,如果程序员知道应该去查找哪种错误,就可以极大地加速诊断过程。
|
||||
**为了捕获失败,异常的细节信息应该包含“对该异常有贡献”的所有参数和字段的值。** 例如, `IndexOutOfBoundsException` 异常的细节消息应该包含下界、上界以及没有落在界内的下标值。该细节消息提供了许多关于失败的信息。这三个值中任何一个或者全部都有可能是错的。实际的下标值可能小于下界或等于上界(“越界错误”),或者它可能是个无效值,太小或太大。下界也有可能大于上界(严重违反内部约束条件的一种情况) 。每一种情形都代表了不同的问题,如果程序员知道应该去查找哪种错误,就可以极大地加速诊断过程。
|
||||
|
||||
对安全敏感的信息有一条忠告。由于在诊断和修正软件问题的过程中,许多人都可以看见堆栈轨迹, **因此千万不要在细节消息中包含密码、密钥以及类似的信息!**
|
||||
|
||||
@ -10,7 +10,7 @@
|
||||
|
||||
异常的细节消息不应该与“用户层次的错误消息”混为一谈,后者对于最终用户而言必须是可理解的。与用户层次的错误消息不同,异常的字符串表示法主要是让程序员或者网站可靠性工程师用来分析失败的原因。因此,信息的内容比可读性要重要得多。用户层次的错误消息经常被本地化,而异常的细节消息则几乎没有被本地化。
|
||||
|
||||
为了确保在异常的细节消息中包含足够的失败- 捕捉信息, 一种办法是在异常的构造器而不是字符串细节消息中引人这些信息。然后,有了这些信息,只要把它们放到消息描述中,就可以自动产生细节消息。例如 IndexOutOfBoundsException 使用如下构造器代替 String 构造器:
|
||||
为了确保在异常的细节消息中包含足够的失败- 捕捉信息, 一种办法是在异常的构造器而不是字符串细节消息中引人这些信息。然后,有了这些信息,只要把它们放到消息描述中,就可以自动产生细节消息。例如 `IndexOutOfBoundsException` 使用如下构造器代替 `String` 构造器:
|
||||
|
||||
```java
|
||||
/**
|
||||
@ -33,6 +33,6 @@ public IndexOutOfBoundsException( int lowerBound, int upperBound,
|
||||
}
|
||||
```
|
||||
|
||||
从 Java 9 开始, IndexOutOfBoundsException 终于获得了一个构造器,它可以带一个类型为 int 的 index 参数值,但遗憾的是,它删去了 lowerBound 和 upperBound 参数。更通俗地说, Java 平台类库并没有广泛地使用这种做法,但是,这种做法仍然值得大力推荐。它使程序员更加易于抛出异常以捕获失败。实际上,这种做法使程序员不想捕获失败都难! 这种做法可以有效地把代码集中起来放在异常类中,由这些代码对异常类自身中的异常产生高质量的细节消息,而不是要求类的每个用户都多余地产生细节消息。
|
||||
从 Java 9 开始, `IndexOutOfBoundsException` 终于获得了一个构造器,它可以带一个类型为 int 的 index 参数值,但遗憾的是,它删去了 lowerBound 和 upperBound 参数。更通俗地说, Java 平台类库并没有广泛地使用这种做法,但是,这种做法仍然值得大力推荐。它使程序员更加易于抛出异常以捕获失败。实际上,这种做法使程序员不想捕获失败都难! 这种做法可以有效地把代码集中起来放在异常类中,由这些代码对异常类自身中的异常产生高质量的细节消息,而不是要求类的每个用户都多余地产生细节消息。
|
||||
|
||||
正如第 70 条中所建议的, 为异常的失败- 捕获信息 (在上述例子中为 lowerBound 、upperBound 和 index ) 提供一些访问方法是合适的。提供这样的访问方法对受检的异常,比对未受检异常更为重要,因为失败一捕获信息对于从失败中恢复是非常有用的。程序员希望通过程序的手段来访问未受检异常的细节,这很少见 (尽管也是可以想象的) 。然而,即使对于未受检异常,作为一般原则提供这些访问方法也是明智的 (详见第 12 条) 。
|
||||
正如第 70 条中所建议的, 为异常的失败- 捕获信息(在上述例子中为 lowerBound 、upperBound 和 index)提供一些访问方法是合适的。提供这样的访问方法对受检的异常,比对未受检异常更为重要,因为失败一捕获信息对于从失败中恢复是非常有用的。程序员希望通过程序的手段来访问未受检异常的细节,这很少见 (尽管也是可以想象的) 。然而,即使对于未受检异常,作为一般原则提供这些访问方法也是明智的 (详见第 12 条) 。
|
||||
|
@ -1,10 +1,10 @@
|
||||
# 76. 保持失败原子性
|
||||
|
||||
当对象抛出异常之后,通常我们期望这个对象仍然保持在一种定义良好的可用状态之中, 即使失败是发生在执行某个操作的过程中间。对于受检异常而言,这尤为重要,因为调用者期望能从这种异常中进行恢复。**一般而言,失败的方法调用应该使对象保持在被调用之前的状态。** 具有这种属性的方法被称为具有失败原子性 (failure atomic) 。
|
||||
当对象抛出异常之后,通常我们期望这个对象仍然保持在一种定义良好的可用状态之中, 即使失败是发生在执行某个操作的过程中间。对于受检异常而言,这尤为重要,因为调用者期望能从这种异常中进行恢复。**一般而言,失败的方法调用应该使对象保持在被调用之前的状态。** 具有这种属性的方法被称为具有失败原子性 (failure atomic)。
|
||||
|
||||
有几种途径可以实现这种效果。最简单的办法莫过于设计一个不可变的对象 (详见第 17 条) 。如果对象是不可变的,失败原子性就是显然的。如果一个操作失败了,它可能会阻止创建新的对象,但是永远也不会使已有的对象保持在不一致的状态之中,因为当每个对象被创建之后它就处于一致的状态之中,以后也不会再发生变化。
|
||||
有几种途径可以实现这种效果。最简单的办法莫过于设计一个不可变的对象 (详见第 17 条) 。如果对象是不可变的,失败原子性就是显然的。如果一个操作失败了,它可能会阻止创建新的对象,但是永远也不会使已有的对象保持在不一致的状态之中,因为当每个对象被创建之后它就处于一致的状态之中,以后也不会再发生变化。
|
||||
|
||||
对于在可变对象上执行操作的方法,获得失败原子性最常见的办法是,在执行操作之前检查参数的有效性 (详见第 49 条) 。这可以使得在对象的状态被修改之前,先抛出适当的异常。比如,以第 7 条中的 Stack.pop 方法为例:
|
||||
对于在可变对象上执行操作的方法,获得失败原子性最常见的办法是,在执行操作之前检查参数的有效性 (详见第 49 条)。这可以使得在对象的状态被修改之前,先抛出适当的异常。比如,以第 7 条中的 Stack.pop 方法为例:
|
||||
|
||||
```java
|
||||
public Object pop() {
|
||||
@ -16,15 +16,15 @@ public Object pop() {
|
||||
}
|
||||
```
|
||||
|
||||
如果取消对初始大小 ( size ) 的检查,当这个方法企图从一个空栈中弹出元素时,它仍然会抛出异常。然而,这将会导致 size 字段保持在不一致的状态 (负数) 之中,从而导致将来对该对象的任何方法调用都会失败。此外,那时, pop 方法抛出的 ArrayIndexOutOfBoundsException 异常对于该抽象来说也是不恰当的 (详见第 73 条) 。
|
||||
如果取消对初始大小(size)的检查,当这个方法企图从一个空栈中弹出元素时,它仍然会抛出异常。然而,这将会导致 size 字段保持在不一致的状态(负数)之中,从而导致将来对该对象的任何方法调用都会失败。此外,那时, pop 方法抛出的 `ArrayIndexOutOfBoundsException` 异常对于该抽象来说也是不恰当的(详见第 73 条)。
|
||||
|
||||
一种类似的获得失败原子性的办法是,调整计算处理过程的顺序,使得任何可能会失败的计算部分都在对象状态被修改之前发生。如果对参数的检查只有在执行了部分计算之后才能进行,这种办法实际上就是上一种办法的自然扩展。比如,以 TreeMap 的情形为例,它的元素被按照某种特定的顺序做了排序。为了向 TreeMap 中添加元素,该元素的类型就必须是可以利用 TreeMap 的排序准则与其他元素进行比较的。如果企图增加类型不正确的元素,在 tree 以任何方式被修改之前,自然会导致 ClassCastException 异常。
|
||||
一种类似的获得失败原子性的办法是,调整计算处理过程的顺序,使得任何可能会失败的计算部分都在对象状态被修改之前发生。如果对参数的检查只有在执行了部分计算之后才能进行,这种办法实际上就是上一种办法的自然扩展。比如,以 `TreeMap` 的情形为例,它的元素被按照某种特定的顺序做了排序。为了向 `TreeMap` 中添加元素,该元素的类型就必须是可以利用 `TreeMap` 的排序准则与其他元素进行比较的。如果企图增加类型不正确的元素,在 tree 以任何方式被修改之前,自然会导致 `ClassCastException` 异常。
|
||||
|
||||
第三种获得失败原子性的办法是,在对象的一份临时拷贝上执行操作,当操作完成之后再用临时拷贝中的结果代替对象的内容。如果数据保存在临时的数据结构中,计算过程会更加迅速,使用这种办法就是件很自然的事。例如,有些排序函数会在执行排序之前,先把它的输入列表备份到一个数组中,以便降低在排序的内循环中访问元素所需要的开销。这是出于性能考虑的做法,但是,它增加了一项优势:即使排序失败,它也能保证输入列表保持原样。
|
||||
|
||||
最后一种获得失败原子’性的办法远远没有那么常用,做法是编写一段恢复代码 (recoverycode ),由它来拦截操作过程中发生的失败,以及便对象回滚到操作开始之前的状态上。这种办法主要用于永久性的 (基于磁盘的) 数据结构。
|
||||
最后一种获得失败原子性的办法远远没有那么常用,做法是编写一段恢复代码 (recovery code),由它来拦截操作过程中发生的失败,以及便对象回滚到操作开始之前的状态上。这种办法主要用于永久性的(基于磁盘的) 数据结构。
|
||||
|
||||
虽然一般情况下都希望实现失败原子性,但并非总是可以做到。举个例子,如果两个线程企图在没有适当的同步机制的情况下,并发地修改同一个对象,这个对象就有可能被留在不一致的状态之中。因此,在捕获了 ConcurrentModificationException 异常之后再假设对象仍然是可用的,这就是不正确的。错误通常是不可恢复的,因此,当方法抛出 AssertionError 时,不需要努力去保持失败原子性。
|
||||
虽然一般情况下都希望实现失败原子性,但并非总是可以做到。举个例子,如果两个线程企图在没有适当的同步机制的情况下,并发地修改同一个对象,这个对象就有可能被留在不一致的状态之中。因此,在捕获了 `ConcurrentModificationException` 异常之后再假设对象仍然是可用的,这就是不正确的。错误通常是不可恢复的,因此,当方法抛出 `AssertionError` 时,不需要努力去保持失败原子性。
|
||||
|
||||
即使在可以实现失败原子性的场合,它也并不总是人们所期望的。对于某些操作,它会显著地增加开销或者复杂性。也就是说, 一旦了解了这个问题,获得失败原子性往往既简单又容易。
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user