mirror of
https://github.com/sjsdfg/effective-java-3rd-chinese.git
synced 2025-01-25 11:50:25 +08:00
更新语法高亮
This commit is contained in:
parent
8f4bca743b
commit
917dbb5dad
@ -47,7 +47,7 @@ while (i.hasNext()) { // BUG!
|
||||
|
||||
第二个循环包含一个复制粘贴错误:它初始化一个新的循环变量 i2,但是使用旧的变量 i,不幸的是,它仍在范围内。 生成的代码编译时没有错误,并且在不抛出异常的情况下运行,但它做错了。 第二个循环不是在 c2 上迭代,而是立即终止,给出了 c2 为空的错误印象。 由于程序无声地出错,因此错误可能会长时间无法被检测到。
|
||||
|
||||
如果将类似的复制粘贴错误与 for 循环 (for-each 循环或传统循环) 结合使用,则生成的代码甚至无法编译。第一个循环中的元素 (或迭代器) 变量不在第二个循环中的作用域中。下面是它与传统 for 循环的示例:
|
||||
如果将类似的复制粘贴错误与 for 循环(for-each 循环或传统循环)结合使用,则生成的代码甚至无法编译。第一个循环中的元素(或迭代器)变量不在第二个循环中的作用域中。下面是它与传统 for 循环的示例:
|
||||
|
||||
```java
|
||||
for (Iterator<Element> i = c.iterator(); i.hasNext(); ) {
|
||||
|
@ -38,7 +38,7 @@ public static void main(String[] args) {
|
||||
}
|
||||
```
|
||||
|
||||
不,它不会打印出令人难以置信的东西,但它的行为很奇怪。它在计算表达式 `i==42` 时抛出 NullPointerException。问题是,i 是 Integer,而不是 int 数,而且像所有非常量对象引用字段一样,它的初值为 null。当程序计算表达式 `i==42` 时,它是在比较 Integer 与 int。**在操作中混合使用基本类型和包装类型时,包装类型就会自动拆箱**,这种情况无一例外。如果一个空对象引用自动拆箱,那么你将得到一个 NullPointerException。正如这个程序所演示的,它几乎可以在任何地方发生。修复这个问题非常简单,只需将 i 声明为 int 而不是 Integer。
|
||||
不,它不会打印出令人难以置信的东西,但它的行为很奇怪。它在计算表达式 `i==42` 时抛出 `NullPointerException`。问题是,i 是 Integer,而不是 int 数,而且像所有非常量对象引用字段一样,它的初值为 null。当程序计算表达式 `i==42` 时,它是在比较 Integer 与 int。**在操作中混合使用基本类型和包装类型时,包装类型就会自动拆箱**,这种情况无一例外。如果一个空对象引用自动拆箱,那么你将得到一个 `NullPointerException`。正如这个程序所演示的,它几乎可以在任何地方发生。修复这个问题非常简单,只需将 i 声明为 int 而不是 Integer。
|
||||
|
||||
最后,考虑条目 6 中第 24 页的程序:
|
||||
|
||||
@ -59,4 +59,5 @@ public static void main(String[] args) {
|
||||
|
||||
那么,什么时候应该使用包装类型呢?它们有几个合法的用途。第一个是作为集合中的元素、键和值。不能将基本类型放在集合中,因此必须使用包装类型。这是一般情况下的特例。在参数化类型和方法(Chapter 5)中,必须使用包装类型作为类型参数,因为 Java 不允许使用基本类型。例如,不能将变量声明为 `ThreadLocal<int>` 类型,因此必须使用 `ThreadLocal<Integer>`。最后,在进行反射方法调用时,必须使用包装类型(详见第 65 条)。
|
||||
|
||||
总之,只要有选择,就应该优先使用基本类型,而不是包装类型。基本类型更简单、更快。如果必须使用包装类型,请小心!**自动装箱减少了使用包装类型的冗长,但没有减少危险。** 当你的程序使用 `==` 操作符比较两个包装类型时,它会执行标识比较,这几乎肯定不是你想要的。当你的程序执行包含包装类型和基本类型的混合类型计算时,它将进行拆箱,**当你的程序执行拆箱时,将抛出 NullPointerException。** 最后,当你的程序将基本类型装箱时,可能会导致代价高昂且不必要的对象创建。
|
||||
总之,只要有选择,就应该优先使用基本类型,而不是包装类型。基本类型更简单、更快。如果必须使用包装类型,请小心!**自动装箱减少了使用包装类型的冗长,但没有减少危险。** 当你的程序使用 `==` 操作符比较两个包装类型时,它会执行标识比较,这几乎肯定不是你想要的。当你的程序执行包含包装类型和基本类型的混合类型计算时,它将进行拆箱,**当你的程序执行拆箱时,将抛出 `NullPointerException`。** 最后,当你的程序将基本类型装箱时,可能会导致代价高昂且不必要的对象创建。
|
||||
|
||||
|
@ -22,16 +22,16 @@ Set<Son> sonSet = new HashSet<>();
|
||||
|
||||
所有的代码都会继续工作。周围的代码不知道旧的实现类型,所以它不会在意更改。
|
||||
|
||||
有一点值得注意:如果原实现提供了接口的通用约定不需要的一些特殊功能,并且代码依赖于该功能,那么新实现提供相同的功能就非常重要。例如,如果围绕第一个声明的代码依赖于 LinkedHashSet 的排序策略,那么在声明中将 HashSet 替换为 LinkedHashSet 将是不正确的,因为 HashSet 不保证迭代顺序。
|
||||
有一点值得注意:如果原实现提供了接口的通用约定不需要的一些特殊功能,并且代码依赖于该功能,那么新实现提供相同的功能就非常重要。例如,如果围绕第一个声明的代码依赖于 `LinkedHashSet` 的排序策略,那么在声明中将 `HashSet` 替换为 `LinkedHashSet` 将是不正确的,因为 `HashSet` 不保证迭代顺序。
|
||||
|
||||
那么,为什么要更改实现类型呢?因为第二个实现比原来的实现提供了更好的性能,或者因为它提供了原来的实现所缺乏的理想功能。例如,假设一个字段包含一个 HashMap 实例。将其更改为 EnumMap 将为迭代提供更好的性能和与键的自然顺序,但是你只能在键类型为 enum 类型的情况下使用 EnumMap。将 HashMap 更改为 LinkedHashMap 将提供可预测的迭代顺序,性能与 HashMap 相当,而不需要对键类型作出任何特殊要求。
|
||||
那么,为什么要更改实现类型呢?因为第二个实现比原来的实现提供了更好的性能,或者因为它提供了原来的实现所缺乏的理想功能。例如,假设一个字段包含一个 `HashMap` 实例。将其更改为 `EnumMap` 将为迭代提供更好的性能和与键的自然顺序,但是你只能在键类型为 enum 类型的情况下使用 `EnumMap`。将 `HashMap` 更改为 `LinkedHashMap` 将提供可预测的迭代顺序,性能与 `HashMap` 相当,而不需要对键类型作出任何特殊要求。
|
||||
|
||||
你可能认为使用变量的实现类型声明变量是可以的,因为你可以同时更改声明类型和实现类型,但是不能保证这种更改会正确编译程序。如果客户端代码对原实现类型使用了替换时不存在的方法,或者客户端代码将实例传递给需要原实现类型的方法,那么在进行此更改之后,代码将不再编译。使用接口类型声明变量可以保持一致。
|
||||
|
||||
**如果没有合适的接口存在,那么用类引用对象是完全合适的。** 例如,考虑值类,如 String 和 BigInteger。值类很少在编写时考虑到多个实现。它们通常是 final 的,很少有相应的接口。使用这样的值类作为参数、变量、字段或返回类型非常合适。
|
||||
**如果没有合适的接口存在,那么用类引用对象是完全合适的。** 例如,考虑值类,如 `String` 和 `BigInteger`。值类很少在编写时考虑到多个实现。它们通常是 final 的,很少有相应的接口。使用这样的值类作为参数、变量、字段或返回类型非常合适。
|
||||
|
||||
没有合适接口类型的第二种情况是属于框架的对象,框架的基本类型是类而不是接口。如果一个对象属于这样一个基于类的框架,那么最好使用相关的基类来引用它,这通常是抽象的,而不是使用它的实现类。在 java.io 类中许多诸如 OutputStream 之类的就属于这种情况。
|
||||
没有合适接口类型的第二种情况是属于框架的对象,框架的基本类型是类而不是接口。如果一个对象属于这样一个基于类的框架,那么最好使用相关的基类来引用它,这通常是抽象的,而不是使用它的实现类。在 `java.io` 类中许多诸如 `OutputStream` 之类的就属于这种情况。
|
||||
|
||||
没有合适接口类型的最后一种情况是,实现接口但同时提供接口中不存在的额外方法的类,例如,PriorityQueue 有一个在 Queue 接口上不存在的比较器方法。只有当程序依赖于额外的方法时,才应该使用这样的类来引用它的实例,这种情况应该非常少见。
|
||||
没有合适接口类型的最后一种情况是,实现接口但同时提供接口中不存在的额外方法的类,例如,`PriorityQueue` 有一个在 `Queue` 接口上不存在的比较器方法。只有当程序依赖于额外的方法时,才应该使用这样的类来引用它的实例,这种情况应该非常少见。
|
||||
|
||||
这三种情况并不是面面俱到的,而仅仅是为了传达适合通过类引用对象的情况。在实际应用中,给定对象是否具有适当的接口应该是显而易见的。如果是这样,如果使用接口引用对象,程序将更加灵活和流行。**如果没有合适的接口,就使用类层次结构中提供所需功能的最底层的类**
|
@ -12,7 +12,7 @@ try {
|
||||
}
|
||||
```
|
||||
|
||||
这段代码有什用,看起来根本不明显,这正是它没有真正被使用的原因(详见 67 条)。事实证明,作为一个要对数组元素进行遍历的实现方式,它的构想是十分拙劣的。当这个循环企图访问数组边界之外的第一个数组元素的时候,使用 try-catch 并且忽略 ArrayIndexOutOfBoundsException 异常的手段来达到终止无限循环的目的。假定它与数组循环的标准模式是等价的,对于它的标准模式每个 Java 程序员都可以一眼辨认出来:
|
||||
这段代码有什用,看起来根本不明显,这正是它没有真正被使用的原因(详见 67 条)。事实证明,作为一个要对数组元素进行遍历的实现方式,它的构想是十分拙劣的。当这个循环企图访问数组边界之外的第一个数组元素的时候,使用 try-catch 并且忽略 `ArrayIndexOutOfBoundsException` 异常的手段来达到终止无限循环的目的。假定它与数组循环的标准模式是等价的,对于它的标准模式每个 Java 程序员都可以一眼辨认出来:
|
||||
|
||||
```java
|
||||
for ( Mountain m : range )
|
||||
|
@ -8,13 +8,13 @@
|
||||
|
||||
有两种非受检的 throwable:运行时异常和错误。在行为上两者是等同的:它们都是不需要也不应该被捕获的 throwable。如果程序抛出非受检异常或者错误,往往属于不可恢复的情形,程序继续执行下去有害无益。如果程序没有捕捉到这样的 throwable,将会导致当前线程中断(halt),并且出现适当的错误消息。
|
||||
|
||||
**用运行时异常来表明编程错误**。大多数运行时异常都表示前提违例(precondition violations)。所谓前提违例是指 API 的客户没有遵守 API 规范建立的约定。例如,数组访问的预定指明了数组的下标值必须在 0 和数组长度-1 之间。ArrayIndexOutOfBoundsException 表明违反了这个前提。
|
||||
**用运行时异常来表明编程错误**。大多数运行时异常都表示前提违例(precondition violations)。所谓前提违例是指 API 的客户没有遵守 API 规范建立的约定。例如,数组访问的预定指明了数组的下标值必须在 0 和数组长度-1 之间。`ArrayIndexOutOfBoundsException` 表明违反了这个前提。
|
||||
|
||||
这个建议有一个问题:对于要处理可恢复的条件,还是处理编程错误,情况并非总是那么黑白分明。例如,考虑资源枯竭的情形,这可能是由程序错误引起的,比如分配了一块不合理的过大数组,也可能确实是由于资源不足而引起的。如果资源枯竭是由于临时的短缺,或是临时需求太大造成的,这种情况可能是可恢复的。API 设计者需要判断这样的资源枯竭是否允许恢复。如果你相信一种情况可能允许回复,就使用受检异常;如果不是,则使用运行时异常。如果不清楚是否有可能恢复,最好使用非受检异常,原因参见 71 条。
|
||||
|
||||
虽然 JLS(Java 语言规范)并没有要求,但是按照惯例,错误(Error)往往被 JVM 保留下来使用,以表明资源不足、约束失败,或者其他使程序无法继续执行的条件。由于这已经是个几乎被普遍接受的管理,因此最好不需要在实现任何新的 Error 的子类。因此,**你实现的所有非受检的 throwable 都应该是 RuntimeExceptiond 子类**(直接或者间接的)。不仅不应该定义 Error 的子类,也不应该抛出 AssertionError 异常。
|
||||
虽然 JLS(Java 语言规范)并没有要求,但是按照惯例,错误(Error)往往被 JVM 保留下来使用,以表明资源不足、约束失败,或者其他使程序无法继续执行的条件。由于这已经是个几乎被普遍接受的管理,因此最好不需要在实现任何新的 `Error` 的子类。因此,**你实现的所有非受检的 throwable 都应该是 `RuntimeExceptiond` 子类**(直接或者间接的)。不仅不应该定义 `Error` 的子类,也不应该抛出 `AssertionError` 异常。
|
||||
|
||||
要想定义一个不是 Exception、RuntimeException 或者 Error 子类的 throwable,这也是有可能的。JLS 并没有直接规定这样的 throwable,而是隐式的指定了:从行为意义上讲,他们等同于普通的受检异常(即 Exception 的子类,但不是 RuntimeException 的子类)。那么什么时候应该使用这样的 throwable?一句话,永远也不会用到。它与普通的受检异常相比没有任何益处,还会困扰 API 的使用者。
|
||||
要想定义一个不是 `Exception`、`RuntimeException` 或者 `Error` 子类的 throwable,这也是有可能的。JLS 并没有直接规定这样的 throwable,而是隐式的指定了:从行为意义上讲,他们等同于普通的受检异常(即 `Exception` 的子类,但不是 `RuntimeException` 的子类)。那么什么时候应该使用这样的 throwable?一句话,永远也不会用到。它与普通的受检异常相比没有任何益处,还会困扰 API 的使用者。
|
||||
|
||||
API 的设计者往往会忘记,异常也是一个完全意义上的对象,可是在它上面定义任何的方法。这些方法的主要用途是捕获异常的代码提供额外信息,特别是关于引发这个异常条件的信息。如果没有这样的方法,程序员必须要懂的如何解析“该异常的字符串表示法”,以便获得这些额外信息。这是极为不好的做法(详见 12 条)。类很少会指定它们的字符串表示法中的细节,因此对于不同的实现及不同的版本,字符串表示法也会大相径庭。由此可见,“解析异常的字符串表示法”的代码可能是不可移植的,也是非常脆弱的。
|
||||
|
||||
|
@ -2,16 +2,16 @@
|
||||
|
||||
描述一个方法所抛出的异常,是正确使用这个方法时所需文档的重要组成部分。因此,花点时间仔细地为每个方法抛出的异常建立文档是特别重要的。
|
||||
|
||||
**始终要单独地声明受检异常,** 并且利用 Javadoc 的 @throws 标签, **准确地记录下抛出每个异常的条件。** 如果一个公有方法可能抛出多个异常类,则不要使用“快捷方式”声明它会抛出这些异常类的某个超类。永远不要声明一个公有方法直接“ throws Exception”,或者更糟糕的是声明它直接“ throws Throwable ”,这是非常极端的例子。这样的声明不仅没有为程序员提供关于“这个方法能够抛出哪些异常”的任何指导信息,而且大大地妨碍了该方法的使用,因为它实际上掩盖了该方法在同样的执行环境下可能抛出的任何其他异常。这条建议有一个例外,就是 main 方法它可以被安全地声明抛出 Exception ,因为它只通过虚拟机调用。
|
||||
**始终要单独地声明受检异常,** 并且利用 Javadoc 的 `@throws` 标签, **准确地记录下抛出每个异常的条件。** 如果一个公有方法可能抛出多个异常类,则不要使用“快捷方式”声明它会抛出这些异常类的某个超类。永远不要声明一个公有方法直接“ throws Exception”,或者更糟糕的是声明它直接“ throws Throwable ”,这是非常极端的例子。这样的声明不仅没有为程序员提供关于“这个方法能够抛出哪些异常”的任何指导信息,而且大大地妨碍了该方法的使用,因为它实际上掩盖了该方法在同样的执行环境下可能抛出的任何其他异常。这条建议有一个例外,就是 main 方法它可以被安全地声明抛出 Exception ,因为它只通过虚拟机调用。
|
||||
|
||||
虽然 Java 语言本身并没有要求程序员为一个方法声明它可能会抛出的未受检异常,但是,如同受检异常一样,仔细地为它们建立文档是非常明智的。未受检异常通常代表编程上的错误(详见第 70 条),让程序员了解所有这些错误都有助于帮助他们避免犯同样的错误。对于方法可能抛出的未受检异常,如果将这些异常信息很好地组织成列表文档,就可以有效地描述出这个方法被成功执行的前提条件。每个方法的文档应该描述它的前提条件(详见第 56 条),这是很重要的,在文档中记录下未受检异常是满足前提条件的最佳做法。
|
||||
|
||||
对于接口中的方法,在文档中记录下它可能抛出的未受检异常显得尤为重要。这份文档构成了该接口的通用约定(general contract )的一部分,它指定了该接口的多个实现必须遵循的公共行为。
|
||||
|
||||
**使用 Javadoc 的 @throws 标签记录下一个方法可能抛出的每个未受检异常,但是不要使用 throws 关键字将未受检的异常包含在方法的声明中。** 使用 API 的程序员必须知道哪些异常是需要受检的,哪些是不需要受检的,因为他们有责任区分这两种情形。当缺少由 throws 声明产生的方法标头时,由 Javadoc 的 @throws 标签所产生的文档就会提供明显的提示信息,以帮助程序员区分受检异常和未受检异常。
|
||||
**使用 Javadoc 的 `@throws` 标签记录下一个方法可能抛出的每个未受检异常,但是不要使用 throws 关键字将未受检的异常包含在方法的声明中。** 使用 API 的程序员必须知道哪些异常是需要受检的,哪些是不需要受检的,因为他们有责任区分这两种情形。当缺少由 throws 声明产生的方法标头时,由 Javadoc 的 `@throws` 标签所产生的文档就会提供明显的提示信息,以帮助程序员区分受检异常和未受检异常。
|
||||
|
||||
应该注意的是,为每个方法可能抛出的所有未受检异常建立文档是很理想的,但是在实践中并非总能做到这一点。当类被修订之后,如果有个导出方法被修改了,它将会抛出额外的未受检异常,这不算违反源代码或者二进制兼容性。假设一个类调用了另一个独立编写的类中的方法。第一个类的编写者可能会为每个方法抛出的未受检异常仔细地建立文档,但是,如果第二个类被修订了,抛出了额外的未受检异常,很有可能第一个类(它并没有被修订)就会把新的未受检异常传播出去,尽管它并没有声明这些异常。
|
||||
|
||||
**如果一个类中的许多方法出于同样的原因而抛出同一个异常,在该类的文档注释中对这个异常建立文档,这是可以接受的,** 而不是为每个方法单独建立文档。一个常见的例子是 NullPointerException。 若类的文档注释中有这样的描述:"All methods in this class throw a NullPointerException 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 子句中为每个受检异常提供单独的声明,但是不要声明未受检的异常。如果没有为可以抛出的异常建立文档,其他人就很难或者根本不可能有效地使用你的类和接口。
|
@ -12,7 +12,7 @@ try {
|
||||
|
||||
**空的 catch 块会使异常达不到应有的目的,** 即强迫你处理异常的情况。忽略异常就如同忽略火警信号一样——如果把火警信号器关掉了,当真正有火灾发生时,就没有人能看到火警信号了。或许你会侥幸逃过劫难,或许结果将是灾难性的。每当见到空的 catch 块时,应该让警钟长鸣。
|
||||
|
||||
有些情形可以忽略异常。比如,关闭 FileinputStream 的时候。因为你还没有改变文件的状态,因此不必执行任何恢复动作,并且已经从文件中读取到所需要的信息,因此不必终止正在进行的操作。即使在这种情况下,把异常记录下来还是明智的做法,因为如果这些异常经常发生,你就可以调查异常的原因。 **如果选择忽略异常, catch 块中应该包含一条注释,说明为什么可以这么做,并且变量应该命名为 ignored:**
|
||||
有些情形可以忽略异常。比如,关闭 `FileinputStream` 的时候。因为你还没有改变文件的状态,因此不必执行任何恢复动作,并且已经从文件中读取到所需要的信息,因此不必终止正在进行的操作。即使在这种情况下,把异常记录下来还是明智的做法,因为如果这些异常经常发生,你就可以调查异常的原因。 **如果选择忽略异常, catch 块中应该包含一条注释,说明为什么可以这么做,并且变量应该命名为 ignored:**
|
||||
|
||||
```java
|
||||
Future<Integer> f = exec.submit(planarMap::chromaticNumber);
|
||||
|
@ -1,6 +1,6 @@
|
||||
# 78. 同步访问共享的可变数据
|
||||
|
||||
关键字 synchronized 可以保证在同一时刻,只有一个线程可以执行某一个方法,或者某一个代码块。许多程序员把同步的概念仅仅理解为一种互斥( mutual exclusion )的方式,即,当一个对象被一个线程修改的时候,可以阻止另一个线程观察到对象内部不一致的状态。按照这种观点,对象被创建的时候处于一致的状态(详见第 17 条),当有方法访问它的时候,它就被锁定了。这些方法观察到对象的状态,并且可能会引起状态转变( statetransition ),即把对象从一种一致的状态转换到另一种一致的状态。正确地使用同步可以保证没有任何方法会看到对象处于不一致的状态中。
|
||||
关键字 `synchronized` 可以保证在同一时刻,只有一个线程可以执行某一个方法,或者某一个代码块。许多程序员把同步的概念仅仅理解为一种互斥( mutual exclusion )的方式,即,当一个对象被一个线程修改的时候,可以阻止另一个线程观察到对象内部不一致的状态。按照这种观点,对象被创建的时候处于一致的状态(详见第 17 条),当有方法访问它的时候,它就被锁定了。这些方法观察到对象的状态,并且可能会引起状态转变( statetransition ),即把对象从一种一致的状态转换到另一种一致的状态。正确地使用同步可以保证没有任何方法会看到对象处于不一致的状态中。
|
||||
|
||||
这种观点是正确的,但是它并没有说明同步的全部意义。如果没有同步,一个线程的变化就不能被其他线程看到。同步不仅可以阻止一个线程看到对象处于不一致的状态之中,它还可以保证进入同步方法或者同步代码块的每个线程,都能看到由同一个锁保护的之前所有的修改效果。
|
||||
|
||||
@ -8,7 +8,7 @@
|
||||
|
||||
你可能昕说过,为了提高性能,在读或写原子数据的时候,应该避免使用同步。这个建议是非常危险而错误的。虽然语言规范保证了线程在读取原子数据的时候,不会看到任意的数值,但是它并不保证一个线程写入的值对于另一个线程将是可见的。 **为了在线程之间进行可靠的通信,也为了互斥访问,同步是必要的。** 这归因于 Java 语言规范中的内存模型(memory model),它规定了一个线程所做的变化何时以及如何变成对其他线程可见[JLS ,17.4; Goetz06, 16]。
|
||||
|
||||
如果对共享的可变数据的访问不能同步,其后果将非常可怕,即使这个变量是原子可读写的。以下面这个阻止一个线程妨碍,另一个线程的任务为例。Java 的类库中提供了 Thread . stop 方法,但是在很久以前就不提倡使用该方法了,因为它本质上是不安全的一一使用它会导致数据遭到破坏。 **千万不要使用 Thread.stop 方法。** 要阻止一个线程妨碍另一个线程,建议的做法是让第一个线程轮询( poll ) 一个 boolean 字段,这个字段一开始为 false ,但是可以通过第二个线程设置为 true ,以表示第一个线程将终止自己。由于 boolean 字段的读和写操作都是原子的,程序员在访问这个字段的时候不再需要使用同步:
|
||||
如果对共享的可变数据的访问不能同步,其后果将非常可怕,即使这个变量是原子可读写的。以下面这个阻止一个线程妨碍,另一个线程的任务为例。Java 的类库中提供了 `Thread.stop` 方法,但是在很久以前就不提倡使用该方法了,因为它本质上是不安全的一一使用它会导致数据遭到破坏。 **千万不要使用 `Thread.stop` 方法。** 要阻止一个线程妨碍另一个线程,建议的做法是让第一个线程轮询( poll ) 一个 boolean 字段,这个字段一开始为 false ,但是可以通过第二个线程设置为 true ,以表示第一个线程将终止自己。由于 boolean 字段的读和写操作都是原子的,程序员在访问这个字段的时候不再需要使用同步:
|
||||
|
||||
```java
|
||||
// Broken! - How long would you expect this program to run?
|
||||
@ -76,7 +76,7 @@ public class StopThread {
|
||||
|
||||
注意写方法( requestStop )和读方法( stopRequested )都被同步了。只同步写方法还不够! **除非读和写操作都被同步,否则无法保证同步能起作用。** 有时候,会在某些机器上看到只同步了写(或读)操作的程序看起来也能正常工作,但是在这种情况下,表象具有很大的欺骗性。
|
||||
|
||||
StopThread 中被同步方法的动作即使没有同步也是原子的。换句话说,这些方法的同步只是为了它的通信效果,而不是为了互斥访问。虽然循环的每个迭代中的同步开销很小,还是有其他更正确的替代方法,它更加简洁,性能也可能更好。如果 stopRequested 被声明为 volatile ,第二种版本的 StopThread 中的锁就可以省略。虽然 volatile 修饰符不执行互斥访问,但它可以保证任何一个线程在读取该字段的时候都将看到最近刚刚被写入的值:
|
||||
`StopThread` 中被同步方法的动作即使没有同步也是原子的。换句话说,这些方法的同步只是为了它的通信效果,而不是为了互斥访问。虽然循环的每个迭代中的同步开销很小,还是有其他更正确的替代方法,它更加简洁,性能也可能更好。如果 stopRequested 被声明为 volatile ,第二种版本的 `StopThread` 中的锁就可以省略。虽然 volatile 修饰符不执行互斥访问,但它可以保证任何一个线程在读取该字段的时候都将看到最近刚刚被写入的值:
|
||||
|
||||
```java
|
||||
// Cooperative thread termination with a volatile field
|
||||
@ -111,9 +111,9 @@ public static int generateSerialNumber() {
|
||||
|
||||
问题在于,增量操作符(++)不是原子的。它在 nextSerialNumber 字段中执行两项操作:首先它读取值,然后写回一个新值,相当于原来的值再加上 1。如果第二个线程在第一个线程读取旧值和写回新值期间读取这个字段第二个线程就会与第一个线程一起看到同一个值,并返回相同的序列号。这就是安全性失败( safety failure ):这个程序会计算出错误的结果。
|
||||
|
||||
修正 generateSerialNumber 方法的一种方法是在它的声明中增加 synchronized 修饰符。这样可以确保多个调用不会交叉存取,确保每个调用都会看到之前所有调用的效果。一旦这么做,就可以且应该从 nextSerialNumber 中删除 volatile 修饰符。为了保护这个方法,要用 long 代替 int ,或者在 nextSerialNumber 要进行包装时抛出异常。
|
||||
修正 `generateSerialNumber` 方法的一种方法是在它的声明中增加 `synchronized` 修饰符。这样可以确保多个调用不会交叉存取,确保每个调用都会看到之前所有调用的效果。一旦这么做,就可以且应该从 nextSerialNumber 中删除 volatile 修饰符。为了保护这个方法,要用 long 代替 int ,或者在 nextSerialNumber 要进行包装时抛出异常。
|
||||
|
||||
最好还是遵循第 59 条中的建议,使用 AtomicLong 类,它是 java.util.concurrent.atomic 的组成部分。这个包为在单个变量上进行免锁定、线程安全的编程提供了基本类型。虽然 volatile 只提供了同步的通信效果,但这个包还提供了原子性。这正是你想让 generateSerialNumber 完成的工作,并且它可能比同步版本完成得更好:
|
||||
最好还是遵循第 59 条中的建议,使用 `AtomicLong` 类,它是 `java.util.concurrent.atomic` 的组成部分。这个包为在单个变量上进行免锁定、线程安全的编程提供了基本类型。虽然 volatile 只提供了同步的通信效果,但这个包还提供了原子性。这正是你想让 generateSerialNumber 完成的工作,并且它可能比同步版本完成得更好:
|
||||
|
||||
```java
|
||||
// Lock-free synchronization with java.util.concurrent.atomic
|
||||
@ -128,4 +128,4 @@ public static long generateSerialNumber() {
|
||||
|
||||
让一个线程在短时间内修改一个数据对象,然后与其他线程共享,这是可以接受的,它只同步共享对象引用的动作。然后其他线程没有进一步的同步也可以读取对象,只要它没有再被修改。这种对象被称作高效不可变( effectively immutable ) [Goetz06, 3.5.4] 。将这种对象引用从一个线程传递到其他的线程被称作安全发布( safe publication) [Goetz06, 3.5.3] 。安全发布对象引用有许多种方法:可以将它保存在静态字段巾,作为类初始化的一部分;可以将它保存在 volatile 字段、final 字段或者通过正常锁定访问的字段中;或者可以将它放到并发的集合中(详见第 81 条)。
|
||||
|
||||
总而言之, **当多个线程共享可变数据的时候,每个读或者写数据的线程都必须执行同步。** 如果没有同步,就无法保证一个线程所做的修改可以被另一个线程获知。未能同步共享可变数据会造成程序的活性失败( liveness failure )和安全性失败( safety failure ) 。这样的失败是最难调试的。它们可能是间歇性的,且与时间相关,程序的行为在不同的虚拟机上可能根本不同。如果只需要线程之间的交互通信,而不需要互斥, vo latile 修饰符就是一种可以接受的同步形式,但要正确地使用它可能需要一些技巧。
|
||||
总而言之, **当多个线程共享可变数据的时候,每个读或者写数据的线程都必须执行同步。** 如果没有同步,就无法保证一个线程所做的修改可以被另一个线程获知。未能同步共享可变数据会造成程序的活跃性失败( liveness failure )和安全性失败( safety failure ) 。这样的失败是最难调试的。它们可能是间歇性的,且与时间相关,程序的行为在不同的虚拟机上可能根本不同。如果只需要线程之间的交互通信,而不需要互斥, volatile 修饰符就是一种可以接受的同步形式,但要正确地使用它可能需要一些技巧。
|
@ -63,7 +63,7 @@ public interface SetObserver<E> {
|
||||
|
||||
这个接口的结构与 `BiConsumer<Obser vableSet<E> ,E>` 一样。我们选择定义一个定制的函数接口,因为该接口和方法名称可以提升代码的可读性,且该接口可以发展整合多个回调。也就是说,还可以设置合理的参数来使用 BiConsumer (详见第 44 条)。
|
||||
|
||||
如果只是粗略地检验一下, ObservableSet 会显得很正常。例如,下面的程序打印出 0 ~ 99 的数字:
|
||||
如果只是粗略地检验一下, `ObservableSet` 会显得很正常。例如,下面的程序打印出 0 ~ 99 的数字:
|
||||
|
||||
```java
|
||||
public static void main(String[] args) {
|
||||
@ -74,7 +74,7 @@ public static void main(String[] args) {
|
||||
}
|
||||
```
|
||||
|
||||
现在我们来尝试一些更复杂点的例子。假设我们用一个 addObserver 调用来代替这个调用,用来替换的那个 addObserver 调用传递了一个打印 Integer 值的观察者,这个值被添加到该集合中,如果值为 23 ,这个观察者要将自身删除:
|
||||
现在我们来尝试一些更复杂点的例子。假设我们用一个 `addObserver` 调用来代替这个调用,用来替换的那个 `addObserver` 调用传递了一个打印 `Integer` 值的观察者,这个值被添加到该集合中,如果值为 23 ,这个观察者要将自身删除:
|
||||
|
||||
```java
|
||||
set.addObserver(new SetObserver<>() {
|
||||
@ -86,11 +86,11 @@ set.addObserver(new SetObserver<>() {
|
||||
});
|
||||
```
|
||||
|
||||
注意,这个调用以一个匿名类 SetObserver 实例代替了前一个调用中使用的 lambda 。这是因为函数对象需要将自身传给 s.removeObserver ,而 lambda 则无法访问它们自己(详见第 42 条) 。
|
||||
注意,这个调用以一个匿名类 `SetObserver` 实例代替了前一个调用中使用的 lambda 。这是因为函数对象需要将自身传给 `s.removeObserver` ,而 lambda 则无法访问它们自己(详见第 42 条) 。
|
||||
|
||||
你可能以为这个程序会打印数字 0 ~ 23 ,之后观察者会取消预订,程序会悄悄地完成它的工作。实际上却是打印出数字 0 ~ 23 ,然后抛出 ConcurrentModificationException 。问题在于,当 notifyElementAdded 调用观察者的 added 方法时,它正处于遍历 observers 列表的过程中。added 方法调用可观察集合的 removeObserver 方法,从而调用 observers.remove 。现在我们有麻烦了。我们正企图在遍历列表的过程中,将一个元素从列表中删除,这是非法的。notifyElementAdded 方法中的迭代是在一个同步的块中,可以防止并发的修改,但是无法防止迭代线程本身回调到可观察的集合中,也无法防止修改它的 observers 列表。
|
||||
你可能以为这个程序会打印数字 0 ~ 23 ,之后观察者会取消预订,程序会悄悄地完成它的工作。实际上却是打印出数字 0 ~ 23 ,然后抛出 `ConcurrentModificationException` 。问题在于,当 `notifyElementAdded` 调用观察者的 added 方法时,它正处于遍历 observers 列表的过程中。added `方法`调用可观察集合的 `removeObserver` 方法,从而调用 `observers.remove` 。现在我们有麻烦了。我们正企图在遍历列表的过程中,将一个元素从列表中删除,这是非法的。`notifyElementAdded` 方法中的迭代是在一个同步的块中,可以防止并发的修改,但是无法防止迭代线程本身回调到可观察的集合中,也无法防止修改它的 observers 列表。
|
||||
|
||||
现在我们要尝试一些比较奇特的例子: 我们来编写一个试图取消预订的观察者,但是不直接调用 removeObserver ,它用另一个线程的服务来完成。这个观察者使用了一个 executor service (详见第 80 条):
|
||||
现在我们要尝试一些比较奇特的例子: 我们来编写一个试图取消预订的观察者,但是不直接调用 `removeObserver` ,它用另一个线程的服务来完成。这个观察者使用了一个 executor service (详见第 80 条):
|
||||
|
||||
```java
|
||||
// Observer that uses a background thread needlessly
|
||||
@ -116,13 +116,13 @@ set.addObserver(new SetObserver<>() {
|
||||
|
||||
顺便提一句,注意看这个程序在一个 catch 子句中捕获了两个不同的异常类型。这个机制是在 Java 7 中增加的,不太正式地称之为多重捕获( multi-catch ) 。它可以极大地提升代码的清晰度,行为与多异常类型相同的程序,其篇幅可以大幅减少。
|
||||
|
||||
运行这个程序时,没有遇到异常,而是遭遇了死锁。后台线程调用 s .removeObserver,它企图锁定 observers,但它无法获得该锁,因为主线程已经有锁了。在这期间,主线程一直在等待后台线程来完成对观察者的删除,这正是造成死锁的原因。
|
||||
运行这个程序时,没有遇到异常,而是遭遇了死锁。后台线程调用 `s.removeObserver`,它企图锁定 observers,但它无法获得该锁,因为主线程已经有锁了。在这期间,主线程一直在等待后台线程来完成对观察者的删除,这正是造成死锁的原因。
|
||||
|
||||
这个例子是刻意编写用来示范的,因为观察者实际上没理由使用后台线程,但这个问题却是真实的。从同步区域中调用外来方法,在真实的系统中已经造成了许多死锁,例如 GUI 工具箱。
|
||||
|
||||
在前面这两个例子中(异常和死锁),我们都还算幸运。调用外来方法( added )时,同步区域( observers )所保护的资源处于一致的状态。假设当同步区域所保护的约束条件暂时无效时,你要从同步区域中调用一个外来方法。由于 Java 程序设计语言中的锁是可重入的( reentrant ),这种调用不会死锁。就像在第一个例子中一样,它会产生一个异常,因为调用线程已经有这个锁了,因此当该线程试图再次获得该锁时会成功,尽管概念上不相关的另一项操作正在该锁所保护的数据上进行着。这种失败的后果可能是灾难性的。从本质上来说,这个锁没有尽到它的职责。可重入的锁简化了多线程的面向对象程序的构造,但是它们可能会将活性失败变成安全性失败。
|
||||
|
||||
幸运的是,通过将外来方法的调用移出同步的代码块来解决这个问题通常并不太困难。对于 notifyElementAdded 方法,这还涉及给 observers 列表拍张“快照”,然后没有锁也可以安全地遍历这个列表了。经过这一修改,前两个例子运行起来便再也不会出现异常或者死锁了:
|
||||
幸运的是,通过将外来方法的调用移出同步的代码块来解决这个问题通常并不太困难。对于 `notifyElementAdded` 方法,这还涉及给 observers 列表拍张“快照”,然后没有锁也可以安全地遍历这个列表了。经过这一修改,前两个例子运行起来便再也不会出现异常或者死锁了:
|
||||
|
||||
```java
|
||||
// Alien method moved outside of synchronized block - open calls
|
||||
@ -136,9 +136,9 @@ private void notifyElementAdded(E element) {
|
||||
}
|
||||
```
|
||||
|
||||
事实上,要将外来方法的调用移出同步的代码块,还有一种更好的方法。Java 类库提供了一个并发集合( concurrent collection ),详见第 81 条,称作 CopyOnWriteArrayList,这是专门为此定制的。这个 CopyOnWriteArrayList 是 ArrayList 的一种变体,它通过重新拷贝整个底层数组,在这里实现所有的写操作。由于内部数组永远不改动,因此迭代不需要锁定,速度也非常快。如果大量使用, CopyOnWriteArrayList 的性能将大受影响,但是对于观察者列表来说却是很好的,因为它们几乎不改动,并且经常被遍历。
|
||||
事实上,要将外来方法的调用移出同步的代码块,还有一种更好的方法。Java 类库提供了一个并发集合( concurrent collection ),详见第 81 条,称作 `CopyOnWriteArrayList`,这是专门为此定制的。这个 `CopyOnWriteArrayList` 是 `ArrayList` 的一种变体,它通过重新拷贝整个底层数组,在这里实现所有的写操作。由于内部数组永远不改动,因此迭代不需要锁定,速度也非常快。如果大量使用, `CopyOnWriteArrayList` 的性能将大受影响,但是对于观察者列表来说却是很好的,因为它们几乎不改动,并且经常被遍历。
|
||||
|
||||
如果将这个列表改成使用 CopyOnWriteArrayList ,就不必改动 ObservableSet 的 add 和 addAll 方法。下面是这个类的其余代码。注意其中并没有任何显式的同步:
|
||||
如果将这个列表改成使用 `CopyOnWriteArrayList` ,就不必改动 `ObservableSet` 的 `add` 和 `addAll` 方法。下面是这个类的其余代码。注意其中并没有任何显式的同步:
|
||||
|
||||
```java
|
||||
// Thread-safe observable set with CopyOnWriteArrayList
|
||||
@ -164,12 +164,12 @@ private void notifyElementAdded(E element) {
|
||||
|
||||
本条目的第一部分是关于正确性的。接下来,我们要简单地讨论一下性能。虽然自 Java 平台早期以来,同步的成本已经下降了,但更重要的是,永远不要过度同步。在这个多核的时代,过度同步的实际成本并不是指获取锁所花费的 CPU 时间;而是指失去了并行的机会,以及因为需要确保每个核都有一个-致的内存视图而导致的延迟。过度同步的另一项潜在开销在于,它会限制虚拟机优化代码执行的能力。
|
||||
|
||||
如果正在编写一个可变的类,有两种选择:省略所有的同步,如果想要并发使用,就允许客户端在必要的时候从外部同步,或者通过内部同步,使这个类变成是线程安全的(详见第 82 条),你还可以因此获得明显比从外部锁定整个对象更高的并发性。java.util 中的集合(除了已经废弃的 Vector 和 Hashtable 之外)采用了前一种方法,而 java.util.concurrent 中的集合则采用了后一种方法(详见第 81 条)。
|
||||
如果正在编写一个可变的类,有两种选择:省略所有的同步,如果想要并发使用,就允许客户端在必要的时候从外部同步,或者通过内部同步,使这个类变成是线程安全的(详见第 82 条),你还可以因此获得明显比从外部锁定整个对象更高的并发性。`java.util` 中的集合(除了已经废弃的 `Vector` 和 `Hashtable` 之外)采用了前一种方法,而 `java.util.concurrent` 中的集合则采用了后一种方法(详见第 81 条)。
|
||||
|
||||
在 Java 平台出现的早期,许多类都违背了这些指导方针。例如, StringBuffer 实例几乎总是被用于单个线程之中,而它们执行的却是内部同步。为此, StringBuffer 基本上都由 StringBuilder 代替,它是一个非同步的 StringBuf fer 。同样地,java.util.Random 中线程安全的伪随机数生成器,被 java.util.concurrent.ThreadLocalRandom 中非同步的实现取代,主要也是出于上述原因。当你不确定的时候,就不要同步类,而应该建立文档,注明它不是线程安全的。
|
||||
在 Java 平台出现的早期,许多类都违背了这些指导方针。例如, `StringBuffer` 实例几乎总是被用于单个线程之中,而它们执行的却是内部同步。为此, `StringBuffer` 基本上都由 `StringBuilder` 代替,它是一个非同步的 `StringBuffer` 。同样地,`java.util.Random` 中线程安全的伪随机数生成器,被 `java.util.concurrent.ThreadLocalRandom` 中非同步的实现取代,主要也是出于上述原因。当你不确定的时候,就不要同步类,而应该建立文档,注明它不是线程安全的。
|
||||
|
||||
如果你在内部同步了类,就可以使用不同的方法来实现高并发性,例如分拆锁、分离锁和非阻塞并发控制。这些方法都超出了本书的讨论范围,但有其他著作对此进行了阐述[Goetz06, Herlihy12] 。
|
||||
|
||||
如果方法修改了静态字段,并且该方法很可能要被多个线程调用,那么也必须在内部同步对这个字段的访问(除非这个类能够容忍不确定的行为) 。多线程的客户端要在这种方法上执行外部同步是不可能的,因为其他不相关的客户端不需要同步也能调用该方法。字段本质上就是一个全局变量,即使是私有的也一样,因为它可以被不相关的客户端读取和修改。第 78 条中的 generateSerialNumber 方法使用的 nextSerialNumber 字段就是这样的一个例子。
|
||||
如果方法修改了静态字段,并且该方法很可能要被多个线程调用,那么也必须在内部同步对这个字段的访问(除非这个类能够容忍不确定的行为) 。多线程的客户端要在这种方法上执行外部同步是不可能的,因为其他不相关的客户端不需要同步也能调用该方法。字段本质上就是一个全局变量,即使是私有的也一样,因为它可以被不相关的客户端读取和修改。第 78 条中的 `generateSerialNumber` 方法使用的 `nextSerialNumber` 字段就是这样的一个例子。
|
||||
|
||||
总而言之,为了避免死锁和数据破坏,千万不要从同步区字段内部调用外来方法。更通俗地讲,要尽量将同步区字段内部的工作量限制到最少。当你在设计一个可变类的时候,要考虑一下它们是否应该自己完成同步操作。在如今这个多核的时代,这比永远不要过度同步来得更重要。只有当你有足够的理由一定要在内部同步类的时候,才应该这么做,同时还应该将这个决定清楚地写到文档中(详见第 82 条) 。
|
@ -2,7 +2,7 @@
|
||||
|
||||
本书第 1 版中阐述了简单的工作队列(work queue)[Bloch01 ,详见第 49 条]的代码。这个类允许客户端按队列等待由后台线程异步处理的工作项目。当不再需要这个工作队列时,客户端可以调用一个方法,让后台线程在完成了已经在队列中的所有工作之后,优雅地终止自己。这个实现几乎就像一件玩具,但即使如此,它还是需要一整页精细的代码,如果你实现的不对,就容易出现安全问题或者导致活性失败。幸运的是,你再也不需要编写这样的代码了。
|
||||
|
||||
到本书第二版出版的时候, Java 平台中已经增加了 java.util.concurrent 。这个字包中包含了一个 Executor Framework 它是一个很灵活的基于接口的任务执行工具。它创建了一个在各方面都比本书第一版更好的工作队列,却只需要这一行代码:
|
||||
到本书第二版出版的时候, Java 平台中已经增加了 `java.util.concurrent` 。这个字包中包含了一个 Executor Framework 它是一个很灵活的基于接口的任务执行工具。它创建了一个在各方面都比本书第一版更好的工作队列,却只需要这一行代码:
|
||||
|
||||
```java
|
||||
ExecutorService exec = Executors.newSingleThreadExecutor();
|
||||
@ -20,15 +20,15 @@ exec.execute(runnable);
|
||||
exec.shutdown();
|
||||
```
|
||||
|
||||
你可以利用 executor service 完成更多的工作。例如,可以等待完成一项特殊的任务(就如第 79 条中的 get 方法一样),你可以等待一个任务集合中的任何任务或者所有任务完成(利用 invokeAny 或者 invokeAll 方法),可以等待 executor service 优雅地完成终止(利用 awaitTermination 方法),可以在任务完成时逐个地获取这些任务的结果(利用 ExecutorCompletionService),可以调度在某个特殊的时间段定时运行或者阶段性地运行的任务(利用 ScheduledThreadPoolExecutor),等等。
|
||||
你可以利用 executor service 完成更多的工作。例如,可以等待完成一项特殊的任务(就如第 79 条中的 get 方法一样),你可以等待一个任务集合中的任何任务或者所有任务完成(利用 `invokeAny` 或者 `invokeAll` 方法),可以等待 executor service 优雅地完成终止(利用 `awaitTermination` 方法),可以在任务完成时逐个地获取这些任务的结果(利用 `ExecutorCompletionService`),可以调度在某个特殊的时间段定时运行或者阶段性地运行的任务(利用 `ScheduledThreadPoolExecutor`),等等。
|
||||
|
||||
如果想让不止一个线程来处理来自这个队列的请求,只要调用一个不同的静态工厂,这个工厂创建了一种不同的 executor service ,称作线程池(thread pool ) 。你可以用固定或者可变数目的线程创建一个线程池。java.util.concurrent.Executors 类包含了静态工厂,能为你提供所需的大多数 executor 。然而,如果你想来点特别的,可以直接使用 ThreadPoolExecutor 类。这个类允许你控制线程池操作的几乎每个方面。
|
||||
如果想让不止一个线程来处理来自这个队列的请求,只要调用一个不同的静态工厂,这个工厂创建了一种不同的 executor service ,称作线程池(thread pool ) 。你可以用固定或者可变数目的线程创建一个线程池。`java.util.concurrent.Executors` 类包含了静态工厂,能为你提供所需的大多数 executor 。然而,如果你想来点特别的,可以直接使用 `ThreadPoolExecutor` 类。这个类允许你控制线程池操作的几乎每个方面。
|
||||
|
||||
为特殊的应用程序选择 executor service 是很有技巧的。如果编写的是小程序,或者是轻量负载的服务器,使用 Executors.newCachedThreadPool 通常是个不错的选择,因为它不需要配置,并且一般情况下能够「正确地完成工作」。但是对于大负载的服务器来说,缓存的线程池就不是很好的选择了!在缓存的线程池中,被提交的任务没有排成队列,而是直接交给线程执行。如果没有线程可用,就创建一个新的线程。如果服务器负载得太重,以致它所有的 CPU 都完全被占用了,当有更多的任务时,就会创建更多的线程,这样只会使情况变得更糟。因此,在大负载的产品服务器中,最好使用 Executors.newFixedThreadPool ,它为你提供了一个包含固定线程数目的线程池,或者为了最大限度地控制它,就直接使用 ThreadPoolExecutor 类。
|
||||
为特殊的应用程序选择 executor service 是很有技巧的。如果编写的是小程序,或者是轻量负载的服务器,使用 `Executors.newCachedThreadPool` 通常是个不错的选择,因为它不需要配置,并且一般情况下能够「正确地完成工作」。但是对于大负载的服务器来说,缓存的线程池就不是很好的选择了!在缓存的线程池中,被提交的任务没有排成队列,而是直接交给线程执行。如果没有线程可用,就创建一个新的线程。如果服务器负载得太重,以致它所有的 CPU 都完全被占用了,当有更多的任务时,就会创建更多的线程,这样只会使情况变得更糟。因此,在大负载的产品服务器中,最好使用 `Executors.newFixedThreadPool` ,它为你提供了一个包含固定线程数目的线程池,或者为了最大限度地控制它,就直接使用 `ThreadPoolExecutor` 类。
|
||||
|
||||
不仅应该尽量不要编写自己的工作队列,而且还应该尽量不直接使用线程。当直接使用线程时, Thread 是既充当工作单元,又是执行机制。在 Executor Framework 中,工作单元和执行机制是分开的。现在关键的抽象是工作单元,称作任务(task) 。任务有两种:Runnable 及其近亲 Callable (它与 Runnable 类似,但它会返回值,并且能够抛出任意的异常)。执行任务的通用机制是 executor service 。如果你从任务的角度来看问题,并让一个 executor service 替你执行任务,在选择适当的执行策略方面就获得了极大的灵活性。本质上,Executor 框架执行的功能与 Collections 框架聚合(aggregation)的功能相同。
|
||||
不仅应该尽量不要编写自己的工作队列,而且还应该尽量不直接使用线程。当直接使用线程时, Thread 是既充当工作单元,又是执行机制。在 Executor Framework 中,工作单元和执行机制是分开的。现在关键的抽象是工作单元,称作任务(task) 。任务有两种:`Runnable` 及其近亲 `Callable` (它与 Runnable 类似,但它会返回值,并且能够抛出任意的异常)。执行任务的通用机制是 executor service 。如果你从任务的角度来看问题,并让一个 executor service 替你执行任务,在选择适当的执行策略方面就获得了极大的灵活性。本质上,Executor 框架执行的功能与 `Collections` 框架聚合(aggregation)的功能相同。
|
||||
|
||||
在 Java 7 中, Executor 框架被扩展为支持 fork-join 任务,这些任务是通过一种称作 fork-join 池的特殊 executor 服务运行的。fork-join 任务用 ForkJoinTask 实例表示,可以被分成更小的子任务,包含 ForkJoinPool 的线程不仅要处理这些任务,还要从另一个线程中“偷”任务,以确保所有的线程保持忙碌,从而提高 CPU 使用率、提高吞吐量,并降低延迟。fork-join 任务的编写和调优是很有技巧的。并行流 Parallel streams (详见第 48 条)是在 fork join 池上编写的,我们不费什么力气就能享受到它们的性能优势,前提是假设它们正好适用于我们手边的任务。
|
||||
在 Java 7 中, Executor 框架被扩展为支持 fork-join 任务,这些任务是通过一种称作 fork-join 池的特殊 executor 服务运行的。fork-join 任务用 ForkJoinTask 实例表示,可以被分成更小的子任务,包含 `ForkJoinPool` 的线程不仅要处理这些任务,还要从另一个线程中“偷”任务,以确保所有的线程保持忙碌,从而提高 CPU 使用率、提高吞吐量,并降低延迟。fork-join 任务的编写和调优是很有技巧的。并行流 Parallel streams (详见第 48 条)是在 fork join 池上编写的,我们不费什么力气就能享受到它们的性能优势,前提是假设它们正好适用于我们手边的任务。
|
||||
|
||||
Executor Framework 的完整处理方法超出了本书的讨论范围,但是有兴趣的读者可以参阅《Java Concurrency in Practice》一书[Goetz06] 。
|
||||
|
||||
|
@ -2,17 +2,17 @@
|
||||
|
||||
类在其方法并发使用时的行为是其与客户端约定的重要组成部分。如果你没有记录类在这一方面的行为,那么它的用户将被迫做出假设。如果这些假设是错误的,生成的程序可能缺少足够的同步(详见 78 条)或过度的同步(详见 79 条)。无论哪种情况,都可能导致严重的错误。
|
||||
|
||||
你可能听说过,可以通过在方法的文档中查找 synchronized 修饰符来判断方法是否线程安全。这个观点有好些方面是错误的。在正常操作中,Javadoc 的输出中没有包含同步修饰符,这是有原因的。方法声明中 synchronized 修饰符的存在是实现细节,而不是其 API 的一部分。**它不能可靠地表明方法是线程安全的。**
|
||||
你可能听说过,可以通过在方法的文档中查找 `synchronized` 修饰符来判断方法是否线程安全。这个观点有好些方面是错误的。在正常操作中,Javadoc 的输出中没有包含同步修饰符,这是有原因的。方法声明中 `synchronized` 修饰符的存在是实现细节,而不是其 API 的一部分。**它不能可靠地表明方法是线程安全的。**
|
||||
|
||||
此外,声称 synchronized 修饰符的存在就足以记录线程安全性,这个观点是对线程安全性属性的误解,认为要么全有要么全无。实际上,线程安全有几个级别。**要启用安全的并发使用,类必须清楚地记录它支持的线程安全级别。** 下面的列表总结了线程安全级别。它并非详尽无遗,但涵盖以下常见情况:
|
||||
此外,声称 `synchronized` 修饰符的存在就足以记录线程安全性,这个观点是对线程安全性属性的误解,认为要么全有要么全无。实际上,线程安全有几个级别。**要启用安全的并发使用,类必须清楚地记录它支持的线程安全级别。** 下面的列表总结了线程安全级别。它并非详尽无遗,但涵盖以下常见情况:
|
||||
|
||||
- **不可变的** — 这个类的实例看起来是常量。不需要外部同步。示例包括 String、Long 和 BigInteger(详见第 17 条)。
|
||||
- **无条件线程安全** — 该类的实例是可变的,但是该类具有足够的内部同步,因此无需任何外部同步即可并发地使用该类的实例。例如 AtomicLong 和 ConcurrentHashMap。
|
||||
- **有条件的线程安全** — 与无条件线程安全类似,只是有些方法需要外部同步才能安全并发使用。示例包括 Collections.synchronized 包装器返回的集合,其迭代器需要外部同步。
|
||||
- **非线程安全** — 该类的实例是可变的。要并发地使用它们,客户端必须使用外部同步来包围每个方法调用(或调用序列)。这样的例子包括通用的集合实现,例如 ArrayList 和 HashMap。
|
||||
- **线程对立** — 即使每个方法调用都被外部同步包围,该类对于并发使用也是不安全的。线程对立通常是由于在不同步的情况下修改静态数据而导致的。没有人故意编写线程对立类;此类通常是由于没有考虑并发性而导致的。当发现类或方法与线程不相容时,通常将其修复或弃用。第 78 条中的 generateSerialNumber 方法在没有内部同步的情况下是线程对立的,如第 322 页所述。
|
||||
- **不可变的** — 这个类的实例看起来是常量。不需要外部同步。示例包括 `String`、`Long` 和 `BigInteger`(详见第 17 条)。
|
||||
- **无条件线程安全** — 该类的实例是可变的,但是该类具有足够的内部同步,因此无需任何外部同步即可并发地使用该类的实例。例如 `AtomicLong` 和 `ConcurrentHashMap`。
|
||||
- **有条件的线程安全** — 与无条件线程安全类似,只是有些方法需要外部同步才能安全并发使用。示例包括 `Collections.synchronized` 包装器返回的集合,其迭代器需要外部同步。
|
||||
- **非线程安全** — 该类的实例是可变的。要并发地使用它们,客户端必须使用外部同步来包围每个方法调用(或调用序列)。这样的例子包括通用的集合实现,例如 `ArrayList` 和 `HashMap`。
|
||||
- **线程对立** — 即使每个方法调用都被外部同步包围,该类对于并发使用也是不安全的。线程对立通常是由于在不同步的情况下修改静态数据而导致的。没有人故意编写线程对立类;此类通常是由于没有考虑并发性而导致的。当发现类或方法与线程不相容时,通常将其修复或弃用。第 78 条中的 `generateSerialNumber` 方法在没有内部同步的情况下是线程对立的,如第 322 页所述。
|
||||
|
||||
这些类别(不包括线程对立类)大致对应于《Java Concurrency in Practice》中的线程安全注解,分别为 Immutable、ThreadSafe 和 NotThreadSafe [Goetz06, Appendix A]。上面分类中的无条件线程安全和有条件的线程安全都包含在 ThreadSafe 注解中。
|
||||
这些类别(不包括线程对立类)大致对应于《Java Concurrency in Practice》中的线程安全注解,分别为 `Immutable`、`ThreadSafe` 和 `NotThreadSafe` [Goetz06, Appendix A]。上面分类中的无条件线程安全和有条件的线程安全都包含在 `ThreadSafe` 注解中。
|
||||
|
||||
在文档中记录一个有条件的线程安全类需要小心。你必须指出哪些调用序列需要外部同步,以及执行这些序列必须获得哪些锁(在极少数情况下是锁)。通常是实例本身的锁,但也有例外。例如,`Collections.synchronizedMap` 的文档提到:
|
||||
|
||||
@ -34,7 +34,7 @@ synchronized(m) { // Synchronizing on m, not s!
|
||||
|
||||
类的线程安全的描述通常属于该类的文档注释,但是具有特殊线程安全属性的方法应该在它们自己的文档注释中描述这些属性。没有必要记录枚举类型的不变性。除非从返回类型可以明显看出,否则静态工厂必须记录返回对象的线程安全性,正如 `Collections.synchronizedMap` 所演示的那样。
|
||||
|
||||
当一个类使用公共可访问锁时,它允许客户端自动执行一系列方法调用,但是这种灵活性是有代价的。它与诸如 ConcurrentHashMap 之类的并发集合所使用的高性能内部并发控制不兼容。此外,客户端可以通过长时间持有可公开访问的锁来发起拒绝服务攻击。这可以是无意的,也可以是有意的。
|
||||
当一个类使用公共可访问锁时,它允许客户端自动执行一系列方法调用,但是这种灵活性是有代价的。它与诸如 `ConcurrentHashMap` 之类的并发集合所使用的高性能内部并发控制不兼容。此外,客户端可以通过长时间持有可公开访问的锁来发起拒绝服务攻击。这可以是无意的,也可以是有意的。
|
||||
|
||||
为了防止这种拒绝服务攻击,你可以使用一个私有锁对象,而不是使用同步方法(隐含一个公共可访问的锁):
|
||||
|
||||
@ -44,6 +44,6 @@ synchronized(m) { // Synchronizing on m, not s!
|
||||
|
||||
私有锁对象用法只能在无条件的线程安全类上使用。有条件的线程安全类不能使用这种用法,因为它们必须在文档中记录,在执行某些方法调用序列时要获取哪些锁。
|
||||
|
||||
私有锁对象用法特别适合为继承而设计的类(详见第 19 条)。如果这样一个类要使用它的实例进行锁定,那么子类很容易在无意中干扰基类的操作,反之亦然。通过为不同的目的使用相同的锁,子类和基类最终可能「踩到对方的脚趾头」。这不仅仅是一个理论问题,它就发生在 Thread 类中 [Bloch05, Puzzle 77]。
|
||||
私有锁对象用法特别适合为继承而设计的类(详见第 19 条)。如果这样一个类要使用它的实例进行锁定,那么子类很容易在无意中干扰基类的操作,反之亦然。通过为不同的目的使用相同的锁,子类和基类最终可能「踩到对方的脚趾头」。这不仅仅是一个理论问题,它就发生在 `Thread` 类中 [Bloch05, Puzzle 77]。
|
||||
|
||||
总之,每个类都应该措辞严谨的描述或使用线程安全注解清楚地记录其线程安全属性。synchronized 修饰符在文档中没有任何作用。有条件的线程安全类必须记录哪些方法调用序列需要外部同步,以及在执行这些序列时需要获取哪些锁。如果你编写一个无条件线程安全的类,请考虑使用一个私有锁对象来代替同步方法。这将保护你免受客户端和子类的同步干扰,并为你提供更大的灵活性,以便在后续的版本中采用复杂的并发控制方式。
|
||||
总之,每个类都应该措辞严谨的描述或使用线程安全注解清楚地记录其线程安全属性。`synchronized` 修饰符在文档中没有任何作用。有条件的线程安全类必须记录哪些方法调用序列需要外部同步,以及在执行这些序列时需要获取哪些锁。如果你编写一个无条件线程安全的类,请考虑使用一个私有锁对象来代替同步方法。这将保护你免受客户端和子类的同步干扰,并为你提供更大的灵活性,以便在后续的版本中采用复杂的并发控制方式。
|
@ -1,24 +1,24 @@
|
||||
# 86. 非常谨慎地实现 Serializable
|
||||
|
||||
使类的实例可序列化非常简单,只需实现 Serializable 接口即可。因为这很容易做到,所以有一个普遍的误解,认为序列化只需要程序员付出很少的努力。而事实上要复杂得多。虽然使类可序列化的即时代价可以忽略不计,但长期代价通常是巨大的。
|
||||
使类的实例可序列化非常简单,只需实现 `Serializable` 接口即可。因为这很容易做到,所以有一个普遍的误解,认为序列化只需要程序员付出很少的努力。而事实上要复杂得多。虽然使类可序列化的即时代价可以忽略不计,但长期代价通常是巨大的。
|
||||
|
||||
**实现 Serializable 接口的一个主要代价是,一旦类的实现被发布,它就会降低更改该类实现的灵活性。** 当类实现 Serializable 时,其字节流编码(或序列化形式)成为其导出 API 的一部分。一旦广泛分发了一个类,通常就需要永远支持序列化的形式,就像需要支持导出 API 的所有其他部分一样。如果你不努力设计自定义序列化形式,而只是接受默认形式,则序列化形式将永远绑定在类的原始内部实现上。换句话说,如果你接受默认的序列化形式,类中私有的包以及私有实例字段将成为其导出 API 的一部分,此时最小化字段作用域(详见第 15 条)作为信息隐藏的工具,将失去其有效性。
|
||||
**实现 `Serializable` 接口的一个主要代价是,一旦类的实现被发布,它就会降低更改该类实现的灵活性。** 当类实现 `Serializable` 时,其字节流编码(或序列化形式)成为其导出 API 的一部分。一旦广泛分发了一个类,通常就需要永远支持序列化的形式,就像需要支持导出 API 的所有其他部分一样。如果你不努力设计自定义序列化形式,而只是接受默认形式,则序列化形式将永远绑定在类的原始内部实现上。换句话说,如果你接受默认的序列化形式,类中私有的包以及私有实例字段将成为其导出 API 的一部分,此时最小化字段作用域(详见第 15 条)作为信息隐藏的工具,将失去其有效性。
|
||||
|
||||
如果你接受默认的序列化形式,然后更改了类的内部实现,则会导致与序列化形式不兼容。试图使用类的旧版本序列化实例,再使用新版本反序列化实例的客户端(反之亦然)程序将会失败。当然,可以在维护原始序列化形式的同时更改内部实现(使用 ObjectOutputStream.putFields 或 ObjectInputStream.readFields),但这可能会很困难,并在源代码中留下明显的缺陷。如果你选择使类可序列化,你应该仔细设计一个高质量的序列化形式,以便长期使用(详见第 87 和 90 条)。这样做会增加开发的初始成本,但是这样做是值得的。即使是设计良好的序列化形式,也会限制类的演化;而设计不良的序列化形式,则可能会造成严重后果。
|
||||
如果你接受默认的序列化形式,然后更改了类的内部实现,则会导致与序列化形式不兼容。试图使用类的旧版本序列化实例,再使用新版本反序列化实例的客户端(反之亦然)程序将会失败。当然,可以在维护原始序列化形式的同时更改内部实现(使用 `ObjectOutputStream.putFields` 或 `ObjectInputStream.readFields`),但这可能会很困难,并在源代码中留下明显的缺陷。如果你选择使类可序列化,你应该仔细设计一个高质量的序列化形式,以便长期使用(详见第 87 和 90 条)。这样做会增加开发的初始成本,但是这样做是值得的。即使是设计良好的序列化形式,也会限制类的演化;而设计不良的序列化形式,则可能会造成严重后果。
|
||||
|
||||
可序列化会使类的演变受到限制,施加这种约束的一个简单示例涉及流的唯一标识符,通常称其为串行版本 UID。每个可序列化的类都有一个与之关联的唯一标识符。如果你没有通过声明一个名为 serialVersionUID 的静态 final long 字段来指定这个标识符,那么系统将在运行时对类应用加密散列函数(SHA-1)自动生成它。这个值受到类的名称、实现的接口及其大多数成员(包括编译器生成的合成成员)的影响。如果你更改了其中任何一项,例如,通过添加一个临时的方法,生成的序列版本 UID 就会更改。如果你未能声明序列版本 UID,兼容性将被破坏,从而在运行时导致 InvalidClassException。
|
||||
可序列化会使类的演变受到限制,施加这种约束的一个简单示例涉及流的唯一标识符,通常称其为串行版本 UID。每个可序列化的类都有一个与之关联的唯一标识符。如果你没有通过声明一个名为 `serialVersionUID` 的静态 final long 字段来指定这个标识符,那么系统将在运行时对类应用加密散列函数(SHA-1)自动生成它。这个值受到类的名称、实现的接口及其大多数成员(包括编译器生成的合成成员)的影响。如果你更改了其中任何一项,例如,通过添加一个临时的方法,生成的序列版本 UID 就会更改。如果你未能声明序列版本 UID,兼容性将被破坏,从而在运行时导致 `InvalidClassException`。
|
||||
|
||||
**实现 Serializable 接口的第二个代价是,增加了出现 bug 和安全漏洞的可能性(第85项)。** 通常,对象是用构造函数创建的;序列化是一种用于创建对象的超语言机制。无论你接受默认行为还是无视它,反序列化都是一个「隐藏构造函数」,其他构造函数具有的所有问题它都有。由于没有与反序列化关联的显式构造函数,因此很容易忘记必须让它能够保证所有的不变量都是由构造函数建立的,并且不允许攻击者访问正在构造的对象内部。依赖于默认的反序列化机制,会让对象轻易地遭受不变性破坏和非法访问(详见第 88 条)。
|
||||
**实现 `Serializable` 接口的第二个代价是,增加了出现 bug 和安全漏洞的可能性(第85项)。** 通常,对象是用构造函数创建的;序列化是一种用于创建对象的超语言机制。无论你接受默认行为还是无视它,反序列化都是一个「隐藏构造函数」,其他构造函数具有的所有问题它都有。由于没有与反序列化关联的显式构造函数,因此很容易忘记必须让它能够保证所有的不变量都是由构造函数建立的,并且不允许攻击者访问正在构造的对象内部。依赖于默认的反序列化机制,会让对象轻易地遭受不变性破坏和非法访问(详见第 88 条)。
|
||||
|
||||
**实现 Serializable 接口的第三个代价是,它增加了与发布类的新版本相关的测试负担。** 当一个可序列化的类被修改时,重要的是检查是否可以在新版本中序列化一个实例,并在旧版本中反序列化它,反之亦然。因此,所需的测试量与可序列化类的数量及版本的数量成正比,工作量可能很大。你必须确保「序列化-反序列化」过程成功,并确保它生成原始对象的无差错副本。如果在第一次编写类时精心设计了自定义序列化形式,那么测试的工作量就会减少(详见第 87 和 90 条)。
|
||||
**实现 `Serializable` 接口的第三个代价是,它增加了与发布类的新版本相关的测试负担。** 当一个可序列化的类被修改时,重要的是检查是否可以在新版本中序列化一个实例,并在旧版本中反序列化它,反之亦然。因此,所需的测试量与可序列化类的数量及版本的数量成正比,工作量可能很大。你必须确保「序列化-反序列化」过程成功,并确保它生成原始对象的无差错副本。如果在第一次编写类时精心设计了自定义序列化形式,那么测试的工作量就会减少(详见第 87 和 90 条)。
|
||||
|
||||
**实现 Serializable 接口并不是一个轻松的决定。** 如果一个类要参与一个框架,该框架依赖于 Java 序列化来进行对象传输或持久化,这对于类来说实现 Serializable 接口就是非常重要的。此外,如果类 A 要成为另一个类 B 的一个组件,类 B 必须实现 Serializable 接口,若类 A 可序列化,它就会更易于被使用。然而,与实现 Serializable 相关的代价很多。每次设计一个类时,都要权衡利弊。历史上,像 BigInteger 和 Instant 这样的值类实现了 Serializable 接口,集合类也实现了 Serializable 接口。表示活动实体(如线程池)的类很少情况适合实现 Serializable 接口。
|
||||
**实现 `Serializable` 接口并不是一个轻松的决定。** 如果一个类要参与一个框架,该框架依赖于 Java 序列化来进行对象传输或持久化,这对于类来说实现 `Serializable` 接口就是非常重要的。此外,如果类 A 要成为另一个类 B 的一个组件,类 B 必须实现 `Serializable` 接口,若类 A 可序列化,它就会更易于被使用。然而,与实现 `Serializable` 相关的代价很多。每次设计一个类时,都要权衡利弊。历史上,像 `BigInteger` 和 `Instant` 这样的值类实现了 `Serializable` 接口,集合类也实现了 `Serializable` 接口。表示活动实体(如线程池)的类很少情况适合实现 `Serializable` 接口。
|
||||
|
||||
**为继承而设计的类(详见第 19 条)很少情况适合实现 Serializable 接口,接口也很少情况适合扩展它。** 违反此规则会给扩展类或实现接口的任何人带来很大的负担。有时,违反规则是恰当的。例如,如果一个类或接口的存在主要是为了参与一个要求所有参与者都实现 Serializable 接口的框架,那么类或接口实现或扩展 Serializable 可能是有意义的。
|
||||
**为继承而设计的类(详见第 19 条)很少情况适合实现 `Serializable` 接口,接口也很少情况适合扩展它。** 违反此规则会给扩展类或实现接口的任何人带来很大的负担。有时,违反规则是恰当的。例如,如果一个类或接口的存在主要是为了参与一个要求所有参与者都实现 `Serializable` 接口的框架,那么类或接口实现或扩展 Serializable 可能是有意义的。
|
||||
|
||||
在为了继承而设计的类中,Throwable 类和 Component 类都实现了 Serializable 接口。正是因为 Throwable 实现了 Serializable 接口,RMI 可以将异常从服务器发送到客户端;Component 类实现了 Serializable 接口,因此可以发送、保存和恢复 GUI,但即使在 Swing 和 AWT 的鼎盛时期,这个工具在实践中也很少使用。
|
||||
在为了继承而设计的类中,`Throwable` 类和 `Component` 类都实现了 `Serializable` 接口。正是因为 `Throwable` 实现了 `Serializable` 接口,RMI 可以将异常从服务器发送到客户端;`Component` 类实现了 `Serializable` 接口,因此可以发送、保存和恢复 GUI,但即使在 Swing 和 AWT 的鼎盛时期,这个工具在实践中也很少使用。
|
||||
|
||||
如果你实现了一个带有实例字段的类,它同时是可序列化和可扩展的,那么需要注意几个风险。如果实例字段值上有任何不变量,关键是要防止子类覆盖 finalize 方法,可以通过覆盖 finalize 并声明它为 final 来做到。最后,如果类的实例字段初始化为默认值(整数类型为 0,布尔值为 false,对象引用类型为 null),那么必须添加 readObjectNoData 方法:
|
||||
如果你实现了一个带有实例字段的类,它同时是可序列化和可扩展的,那么需要注意几个风险。如果实例字段值上有任何不变量,关键是要防止子类覆盖 finalize 方法,可以通过覆盖 finalize 并声明它为 final 来做到。最后,如果类的实例字段初始化为默认值(整数类型为 0,布尔值为 false,对象引用类型为 null),那么必须添加 `readObjectNoData` 方法:
|
||||
|
||||
```java
|
||||
// readObjectNoData for stateful extendable serializable classes
|
||||
@ -29,6 +29,7 @@ private void readObjectNoData() throws InvalidObjectException {
|
||||
|
||||
这个方法是在 Java 4 中添加的,涉及将可序列化超类添加到现有可序列化类 [Serialization, 3.5] 的特殊情况。
|
||||
|
||||
关于不实现 Serializable 的决定,有一个警告。如果为继承而设计的类不可序列化,则可能需要额外的工作来编写可序列化的子类。子类的常规反序列化,要求超类具有可访问的无参数构造函数 [Serialization, 1.10]。如果不提供这样的构造函数,子类将被迫使用序列化代理模式(详见第 90 条)。
|
||||
关于不实现 `Serializable` 的决定,有一个警告。如果为继承而设计的类不可序列化,则可能需要额外的工作来编写可序列化的子类。子类的常规反序列化,要求超类具有可访问的无参数构造函数 [Serialization, 1.10]。如果不提供这样的构造函数,子类将被迫使用序列化代理模式(详见第 90 条)。
|
||||
|
||||
**内部类(详见第 24 条)不应该实现 `Serializable`。** 它们使用编译器生成的合成字段存储对外围实例的引用,并存储来自外围的局部变量的值。这些字段与类定义的对应关系,就和没有指定匿名类和局部类的名称一样。因此,内部类的默认序列化形式是不确定的。但是,静态成员类可以实现 `Serializable` 接口。
|
||||
|
||||
**内部类(详见第 24 条)不应该实现 Serializable。** 它们使用编译器生成的合成字段存储对外围实例的引用,并存储来自外围的局部变量的值。这些字段与类定义的对应关系,就和没有指定匿名类和局部类的名称一样。因此,内部类的默认序列化形式是不确定的。但是,静态成员类可以实现 Serializable 接口。
|
@ -1,6 +1,6 @@
|
||||
# 88. 保护性的编写 readObject 方法
|
||||
|
||||
第 50 条介绍了一个不可变的日期范围类,它包含可变的私有变量 Date。该类通过在其构造器和访问方法(accessor)中保护性的拷贝 Date 对象,极力维护其约束条件和不可变性。该类代码如下所示:
|
||||
第 50 条介绍了一个不可变的日期范围类,它包含可变的私有变量 `Date`。该类通过在其构造器和访问方法(accessor)中保护性的拷贝 `Date` 对象,极力维护其约束条件和不可变性。该类代码如下所示:
|
||||
|
||||
```java
|
||||
// Immutable class that uses defensive copying
|
||||
@ -34,11 +34,11 @@ public final class Period {
|
||||
|
||||
假设决定要把这个类左晨可序列化的。因为 Period 对象的物理表示法正好反映了它的逻辑数据内容,所以,使用默认的序列化形式并没有什么不合理的(详见 87 条)。因此,为了使这个类成为可序列化的,似乎你所需要做的也就是在类的声明中增加 implements Serializable 字样。然而,如果你真的这么做,那么这个类就不保证它的关键约束了。
|
||||
|
||||
问题在于 readObject 方法实际上相当于另外一个公有的构造器,它要求同其他构造器一样警惕所有的注意事项。构造器必须检查其参数的有效性(详见 49 条),并且在必要的时候对参数进行保护性拷贝(详见 50 条),同样的,readObject 方法也需要这样做。如果 readObject 方法无法做到这两者之一,对于攻击者来说要违反这个类的约束条件就相对容易很多。
|
||||
问题在于 `readObject` 方法实际上相当于另外一个公有的构造器,它要求同其他构造器一样警惕所有的注意事项。构造器必须检查其参数的有效性(详见 49 条),并且在必要的时候对参数进行保护性拷贝(详见 50 条),同样的,`readObject` 方法也需要这样做。如果 `readObject` 方法无法做到这两者之一,对于攻击者来说要违反这个类的约束条件就相对容易很多。
|
||||
|
||||
不严格的说, readObject 方法是一个“用字节流作为唯一参数”的构造器。在正常使用的情况下,对一个正常构造的实例进行序列化可以产生字节流。但是,当面对一个人工仿造的字节流时, readObject 产生的对象会违反它所属类的约束条件,这时问题就产生了。这种字节流可以用来创建一个不可能的对象(impossible object),这时利用普通构造器无法创建的。
|
||||
不严格的说, `readObject` 方法是一个“用字节流作为唯一参数”的构造器。在正常使用的情况下,对一个正常构造的实例进行序列化可以产生字节流。但是,当面对一个人工仿造的字节流时, `readObject` 产生的对象会违反它所属类的约束条件,这时问题就产生了。这种字节流可以用来创建一个不可能的对象(impossible object),这时利用普通构造器无法创建的。
|
||||
|
||||
假设我们仅仅在 Period 类的声明加上了 implements Serializable 字样。那么这个丑陋的程序代码将会产生一个 Period 实例,他的结束时间比起始时间还早。对于高位 byte 值进行强制类型转换是 Java 缺少 byte 并且做出 byte 类型签名的不幸决定的后果:
|
||||
假设我们仅仅在 Period 类的声明加上了 `implements Serializable` 字样。那么这个丑陋的程序代码将会产生一个 Period 实例,他的结束时间比起始时间还早。对于高位 byte 值进行强制类型转换是 Java 缺少 byte 并且做出 byte 类型签名的不幸决定的后果:
|
||||
|
||||
```java
|
||||
public class BogusPeriod {
|
||||
@ -78,7 +78,7 @@ public class BogusPeriod {
|
||||
|
||||
被用来初始化 serializedForm 的 byte 常量数组是这样产生的:首先对一个正常的 Period 实例进行序列化,然后对得到的字节流进行手工编辑。对于这个例子而言,字节流的细节并不重要,如果你对此十分好奇,可以在《Java Object Serialization Specification》[Serialization, 6] 中查到有关序列化字节流格式的描述信息。如果运行这个程序,它会打印出“ Fri Jan 01 12:00:00 PST 1999 - Sun Jan 01 12:00:00 PST 1984”。主要把 Period 类声明成为可序列化的,这会使我们创建出其违反类约束条件的对象。
|
||||
|
||||
为了修整这个问题,可以为 Period 提供一个 readObject 方法,该方法首先调用 defaultReadObject,然后检查被反序列化之后的对象有效性。如果有效性检查失败,readObject 方法就会抛出一个 InvalidObjectException 异常,这使得反序列化过程不能成功的完成:
|
||||
为了修整这个问题,可以为 `Period` 提供一个 `readObject` 方法,该方法首先调用 `defaultReadObject`,然后检查被反序列化之后的对象有效性。如果有效性检查失败,`readObject` 方法就会抛出一个 `InvalidObjectException` 异常,这使得反序列化过程不能成功的完成:
|
||||
|
||||
```java
|
||||
// readObject method with validity checking - insufficient!
|
||||
@ -91,7 +91,7 @@ private void readObject(ObjectInputStream s)
|
||||
}
|
||||
```
|
||||
|
||||
尽管这样的修成避免了攻击者创建无效的 Period 实例,但是这里依旧隐藏着一个更为微妙的问题。通过伪造字节流,要想创建可变的 Period 实例仍是有可能的,做法事:字节流以一个有效的 Period 实例开头,然后附加上两个额外的引用,指向 Period 实例中两个私有的 Date 字段。攻击者从 ObjectInputStream 读取 Period 实例,然后读取附加在其后面的“恶意编制的对线引用”。这些对象引用使得攻击者能够访问到 Period 对象内部的私有 Date 字段所引用的对象。通过改变这些 Date 实例,攻击者可以改变 Period 实例。如下的类演示了这种攻击方式:
|
||||
尽管这样的修成避免了攻击者创建无效的 `Period` 实例,但是这里依旧隐藏着一个更为微妙的问题。通过伪造字节流,要想创建可变的 `Period` 实例仍是有可能的,做法事:字节流以一个有效的 `Period` 实例开头,然后附加上两个额外的引用,指向 `Period` 实例中两个私有的 `Date` 字段。攻击者从 `ObjectInputStream` 读取 `Period` 实例,然后读取附加在其后面的“恶意编制的对线引用”。这些对象引用使得攻击者能够访问到 `Period` 对象内部的私有 `Date` 字段所引用的对象。通过改变这些 `Date` 实例,攻击者可以改变 `Period` 实例。如下的类演示了这种攻击方式:
|
||||
|
||||
```java
|
||||
public class MutablePeriod {
|
||||
@ -160,9 +160,9 @@ Wed Nov 22 00:21:29 PST 2017 - Wed Nov 22 00:21:29 PST 1978
|
||||
Wed Nov 22 00:21:29 PST 2017 - Sat Nov 22 00:21:29 PST 1969
|
||||
```
|
||||
|
||||
虽然 Period 实例被创建之后,他的约束条件没有被破坏。但是要随意修改它的内部组件仍然是有可能的。一旦攻击者获得了一个可变的 Period 实例,就可以将这个实例传递给一个“安全性依赖于 Period 的不可变性”的类,从而造成更大的危害。这种推断并不牵强:实际上,有许多类的安全性就是依赖于 String 的不可变性。
|
||||
虽然 Period 实例被创建之后,他的约束条件没有被破坏。但是要随意修改它的内部组件仍然是有可能的。一旦攻击者获得了一个可变的 `Period` 实例,就可以将这个实例传递给一个“安全性依赖于 Period 的不可变性”的类,从而造成更大的危害。这种推断并不牵强:实际上,有许多类的安全性就是依赖于 String 的不可变性。
|
||||
|
||||
问题的根源在于,Period 的 readObject 方法并没有完成足够的保护性拷贝。 **当一个对象被反序列化的时候,对于客户端不应该拥有的对象引用,如果那个字段包含了这样的对象引用,就必须做保护性拷贝,这是非常重要的。** 因此,对于每个可序列化的不可变类,如果它好汉了私有的可变字段,那么在它的 readObject 方法中,必须要对这些字段进行保护性拷贝。下面的这些 readObject 方法可以确保 Period 类的约束条件不会遭到破坏,以保持它的不可变性:
|
||||
问题的根源在于,`Period` 的 `readObject` 方法并没有完成足够的保护性拷贝。 **当一个对象被反序列化的时候,对于客户端不应该拥有的对象引用,如果那个字段包含了这样的对象引用,就必须做保护性拷贝,这是非常重要的。** 因此,对于每个可序列化的不可变类,如果它好汉了私有的可变字段,那么在它的 `readObject` 方法中,必须要对这些字段进行保护性拷贝。下面的这些 `readObject` 方法可以确保 `Period` 类的约束条件不会遭到破坏,以保持它的不可变性:
|
||||
|
||||
```java
|
||||
// readObject method with defensive copying and validity checking
|
||||
@ -178,20 +178,20 @@ private void readObject(ObjectInputStream s)
|
||||
}
|
||||
```
|
||||
|
||||
注意,保护性拷贝是在有效性检查之前进行的。我们没有使用 Date 的 clone 方法来执行保护性拷贝机制。这两个细节对于保护 Period 类免受攻击是必要的(详见 50 条)。同时也注意到,对于 final 字段,保护性字段是不可能的。为了使用 readObject 方法,我们必须要将 start 和 end 字段声明成为非 final 的。很遗憾的是,这还算是相对比较好的做法。有了这新的 readObject 方法,并且取消了 start 和 end 的 final 修饰符之后,MutablePeriod 类将不再有效。此时,上面的攻击程序会产生如下输出:
|
||||
注意,保护性拷贝是在有效性检查之前进行的。我们没有使用 `Date` 的 `clone` 方法来执行保护性拷贝机制。这两个细节对于保护 `Period` 类免受攻击是必要的(详见 50 条)。同时也注意到,对于 final 字段,保护性字段是不可能的。为了使用 `readObject` 方法,我们必须要将 start 和 end 字段声明成为非 final 的。很遗憾的是,这还算是相对比较好的做法。有了这新的 `readObject` 方法,并且取消了 start 和 end 的 final 修饰符之后,`MutablePeriod` 类将不再有效。此时,上面的攻击程序会产生如下输出:
|
||||
|
||||
```java
|
||||
Wed Nov 22 00:23:41 PST 2017 - Wed Nov 22 00:23:41 PST 2017
|
||||
Wed Nov 22 00:23:41 PST 2017 - Wed Nov 22 00:23:41 PST 2017
|
||||
```
|
||||
|
||||
有一个简单的“石蕊”测试,可以用来确定默认的 readObject 方法是否可以被接受。测试方法:增加一个公有的构造器,其参数对应于该对象中每个非 transient 的字段,并且无论参数的值是什么,都是不进行检查就可以保存到相应的字段中。对于这样的做法,你是否会感到很舒适?如果你对这个问题的回答是否定的,就必须提供一个显式的 readObject 方法,并且它必须执行构造器所要求的所有有效性检查和保护性拷贝。另一种方法是,可以使用序列化代理模式(serialization proxy pattern),详见第 90 条。强烈建议使用这个模式,因为它分担了安全反序列化的部门工作。
|
||||
有一个简单的“石蕊”测试,可以用来确定默认的 `readObject` 方法是否可以被接受。测试方法:增加一个公有的构造器,其参数对应于该对象中每个非 transient 的字段,并且无论参数的值是什么,都是不进行检查就可以保存到相应的字段中。对于这样的做法,你是否会感到很舒适?如果你对这个问题的回答是否定的,就必须提供一个显式的 `readObject` 方法,并且它必须执行构造器所要求的所有有效性检查和保护性拷贝。另一种方法是,可以使用序列化代理模式(serialization proxy pattern),详见第 90 条。强烈建议使用这个模式,因为它分担了安全反序列化的部门工作。
|
||||
|
||||
对于非 final 的可序列化的类,在 readObject 方法和构造器之间还有其他类似的地方。与构造器一样,readObject 方法不可以调用可被覆盖的方法,无论是直接调用还是间接调用都不可以(详见 19 条)。如果违反了这条规则,并且覆盖了该方法,被覆盖的方法将在子类的状态被反序列化之前先运行。这个程序很可能会失败[Bloch05, Puzzle 91]。
|
||||
对于非 final 的可序列化的类,在 `readObject` 方法和构造器之间还有其他类似的地方。与构造器一样,`readObject` 方法不可以调用可被覆盖的方法,无论是直接调用还是间接调用都不可以(详见 19 条)。如果违反了这条规则,并且覆盖了该方法,被覆盖的方法将在子类的状态被反序列化之前先运行。这个程序很可能会失败[Bloch05, Puzzle 91]。
|
||||
|
||||
总而言之,在编写 readObject 方法的时候,都要这样想:你正在编写一个公有的构造器,无论给它传递什么样的字节流,它都必须产生一个有效的实例。不要假设这个字节流一定代表着一个真正被序列化的实例。虽然在本条目的例子中,类使用了默认的序列化形式,但是所有讨论到的有可能发生的问题也同样适用于自定义序列化形式的类。下面以摘要的形式给出一些指导方针,有助于编写出更健壮的 readObject 方法。
|
||||
总而言之,在编写 `readObject` 方法的时候,都要这样想:你正在编写一个公有的构造器,无论给它传递什么样的字节流,它都必须产生一个有效的实例。不要假设这个字节流一定代表着一个真正被序列化的实例。虽然在本条目的例子中,类使用了默认的序列化形式,但是所有讨论到的有可能发生的问题也同样适用于自定义序列化形式的类。下面以摘要的形式给出一些指导方针,有助于编写出更健壮的 `readObject` 方法。
|
||||
|
||||
- 类中的对象引用字段必须保持为私有属性,要保护性的拷贝这些字段中的每个对象。不可变类中的可变组件就属于这一类别
|
||||
- 对于任何约束条件,如果检查失败就抛出一个 InvalidObjectException 异常。这些检查动作应该跟在所有的保护性拷贝之后。
|
||||
- 如果整个对象图在被反序列化之后必须进行验证,就应该使用 ObjectInputValidation 接口(本书没有讨论)。
|
||||
- 对于任何约束条件,如果检查失败就抛出一个 `InvalidObjectException` 异常。这些检查动作应该跟在所有的保护性拷贝之后。
|
||||
- 如果整个对象图在被反序列化之后必须进行验证,就应该使用 `ObjectInputValidation` 接口(本书没有讨论)。
|
||||
- 无论是直接方法还是间接方法,都不要调用类中任何可被覆盖的方法。
|
||||
|
@ -2,9 +2,9 @@
|
||||
|
||||
正如 85 条和第 86 条提到的,以及本章一直在讨论的,决定实现 Serializable 接口,会增加出错和出现安全问题的可能性,因为它允许利用语言之外的机制来创建实例,而不是使用普通的构造器。然而,有一只方法可以极大的减少这些风险。就是序列化代理模式(seralization proxy pattern)。
|
||||
|
||||
序列化代理模式相当简单。首先,为可序列化的类设计一个私有的静态嵌套类,精确地表示外围类的逻辑状态。这个嵌套类被称为序列化代理(seralization proxy),它应该有一个单独的构造器,其参数类型就是那个外围类。这个构造器只是从它的参数中复制数据:它不需要进行任何一致性检验或者保护性拷贝。从设计的角度看,序列化代理的默认序列化形式是外围类最好的序列化形式。外围类及其序列代理都必须声明实现 Serializable 接口。
|
||||
序列化代理模式相当简单。首先,为可序列化的类设计一个私有的静态嵌套类,精确地表示外围类的逻辑状态。这个嵌套类被称为序列化代理(seralization proxy),它应该有一个单独的构造器,其参数类型就是那个外围类。这个构造器只是从它的参数中复制数据:它不需要进行任何一致性检验或者保护性拷贝。从设计的角度看,序列化代理的默认序列化形式是外围类最好的序列化形式。外围类及其序列代理都必须声明实现 `Serializable` 接口。
|
||||
|
||||
例如,以第 50 条中编写不可变的 Period 类为例,它在第 88 条中被作为可序列化的。以下是一个类的序列化代理。Period 类是如此简单,以致于它的序列化代理有着与类完全相同的字段。
|
||||
例如,以第 50 条中编写不可变的 `Period` 类为例,它在第 88 条中被作为可序列化的。以下是一个类的序列化代理。`Period` 类是如此简单,以致于它的序列化代理有着与类完全相同的字段。
|
||||
|
||||
```java
|
||||
// Serialization proxy for Period class
|
||||
@ -20,7 +20,7 @@ private static class SerializationProxy implements Serializable {
|
||||
}
|
||||
```
|
||||
|
||||
接下来,将下面的 writeReplace 方法添加到外围类中。通过序列化代理,这个方法可以被逐字的复制到任何类中。
|
||||
接下来,将下面的 `writeReplace` 方法添加到外围类中。通过序列化代理,这个方法可以被逐字的复制到任何类中。
|
||||
|
||||
```java
|
||||
// writeReplace method for the serialization proxy pattern
|
||||
@ -29,9 +29,9 @@ private Object writeReplace() {
|
||||
}
|
||||
```
|
||||
|
||||
这个方法的存在就是导致系统产生一个 SerializationProxy 实例,代替外围类的实例。换句话说 writeReplace 方法在序列化之前,将外围类的实例转变成了它的序列化代理。
|
||||
这个方法的存在就是导致系统产生一个 `SerializationProxy` 实例,代替外围类的实例。换句话说 `writeReplace` 方法在序列化之前,将外围类的实例转变成了它的序列化代理。
|
||||
|
||||
有了 writeReplace 方法之后,序列化系统永远不会产生外围类的序列化实例,但是攻击者有可能伪造企图违反该类约束条件的示例。为了防止此类攻击,只需要在外围类中添加如下 readObject 方法。
|
||||
有了 `writeReplace` 方法之后,序列化系统永远不会产生外围类的序列化实例,但是攻击者有可能伪造企图违反该类约束条件的示例。为了防止此类攻击,只需要在外围类中添加如下 `readObject` 方法。
|
||||
|
||||
```java
|
||||
// readObject method for the serialization proxy pattern
|
||||
@ -41,11 +41,11 @@ private void readObject(ObjectInputStream stream)
|
||||
}
|
||||
```
|
||||
|
||||
最后在 SerializationProxy 类中提供一个 readResolve 方法,他返回一个逻辑上等价的外围类的实例。这个方法的出现,导致序列化系统在反序列化的时候将序列化代理转为外围类的实例。
|
||||
最后在 `SerializationProxy` 类中提供一个 `readResolve` 方法,他返回一个逻辑上等价的外围类的实例。这个方法的出现,导致序列化系统在反序列化的时候将序列化代理转为外围类的实例。
|
||||
|
||||
这个 readResolve 方法仅仅利用它的公有 API 创建外围类的一个实例,这正是该模式的魅力所在它极大的消除了序列化机制中语言之外的特征,因为反序列化实例是利用与任何其他实例相同的构造器、静态工厂和方法而创建的。这样你就不必单独确保被反序列化的实例一定要遵守类的约束条件。如果该类的静态工厂或者构造器建立了这些约束条件,并且它的实例方法保持着这些约束条件,你就可以确信序列化也确保着这些约束条件。
|
||||
这个 `readResolve` 方法仅仅利用它的公有 API 创建外围类的一个实例,这正是该模式的魅力所在它极大的消除了序列化机制中语言之外的特征,因为反序列化实例是利用与任何其他实例相同的构造器、静态工厂和方法而创建的。这样你就不必单独确保被反序列化的实例一定要遵守类的约束条件。如果该类的静态工厂或者构造器建立了这些约束条件,并且它的实例方法保持着这些约束条件,你就可以确信序列化也确保着这些约束条件。
|
||||
|
||||
以下是上述的 Period.SerializationProxy 的 readResolve 方法:
|
||||
以下是上述的 `Period.SerializationProxy` 的 `readResolve` 方法:
|
||||
|
||||
```java
|
||||
// readResolve method for Period.SerializationProxy
|
||||
@ -54,13 +54,13 @@ private Object readResolve() {
|
||||
}
|
||||
```
|
||||
|
||||
正如保护性拷贝方法一样(详见 88 条),序列化代理方式可以阻止伪字节流的攻击(详见 88 条)以及内部字段的盗用攻击(详见 88 条)。与前两种方法不同,这种方法允许 Period 类的字段为 final,为了确保 Period 类是真正不可变的(详见 17 条),这一点非常重要。与前两种方法不同的还有,这种方法不需要太费心思。你不必知道哪些字段可能受到狡猾的序列化攻击的威胁,你也不必显式的执行有效性检查,作为反序列化的一部分。
|
||||
正如保护性拷贝方法一样(详见 88 条),序列化代理方式可以阻止伪字节流的攻击(详见 88 条)以及内部字段的盗用攻击(详见 88 条)。与前两种方法不同,这种方法允许 `Period` 类的字段为 final,为了确保 `Period` 类是真正不可变的(详见 17 条),这一点非常重要。与前两种方法不同的还有,这种方法不需要太费心思。你不必知道哪些字段可能受到狡猾的序列化攻击的威胁,你也不必显式的执行有效性检查,作为反序列化的一部分。
|
||||
|
||||
还有另外一种方法,使用这种方法时,序列化代理模式的功能比保护性拷贝的更加强大。序列化代理模式允许反序列化实例有着与原始序列化实例不同的类。你可能认为这在实际应用中没有什么作用,其实不然。
|
||||
|
||||
以 EnumSet 的情况为例(详见 36 条)。这个类没有公有的构造器,只有静态工厂。从客户端的角度来看,他们返回 EnumSet 实例,但是在 OpenJDK 的实现,它们返回的是两种子类之一,具体取决于底层枚举类型的大小。如果底层的枚举类型有 64 个或者少于 64 个的元素,静态工厂就返回一个 RegularEnumSet;它们就返回一个 JunmboEnumSet。
|
||||
以 `EnumSet` 的情况为例(详见 36 条)。这个类没有公有的构造器,只有静态工厂。从客户端的角度来看,他们返回 `EnumSet` 实例,但是在 OpenJDK 的实现,它们返回的是两种子类之一,具体取决于底层枚举类型的大小。如果底层的枚举类型有 64 个或者少于 64 个的元素,静态工厂就返回一个 `RegularEnumSet`;它们就返回一个 `JunmboEnumSet`。
|
||||
|
||||
现在考虑这种情况:如果序列化一个枚举类型,它的枚举有 60 个元素,然后给这个枚举类型再增加 5 个元素,之后反序列化这个枚举集合。当它被序列化的时候,是一个 RegularEnumSet 实例,但是它一旦被反序列化,他就变成了 JunmboEnumSet 实例。实际发生的情况也正是如此,因为 EnumSet 使用序列化代理模式如果你感兴趣,可以看看如下的 EnumSet 序列化代理,它实际上就是这么简单:
|
||||
现在考虑这种情况:如果序列化一个枚举类型,它的枚举有 60 个元素,然后给这个枚举类型再增加 5 个元素,之后反序列化这个枚举集合。当它被序列化的时候,是一个 `RegularEnumSet` 实例,但是它一旦被反序列化,他就变成了 `JunmboEnumSet` 实例。实际发生的情况也正是如此,因为 `EnumSet` 使用序列化代理模式如果你感兴趣,可以看看如下的 `EnumSet` 序列化代理,它实际上就是这么简单:
|
||||
|
||||
```java
|
||||
// EnumSet's serialization proxy
|
||||
@ -90,8 +90,8 @@ private static class SerializationProxy<E extends Enum<E>>
|
||||
}
|
||||
```
|
||||
|
||||
序列化代理模式有两个局限性。它不能与可以被客户端拓展的类兼容(详见 19 条)。它也不能与对象图中包含循环的某些类兼容:如果你企图从一个对象的序列化代理的 readResovle 方法内部调用这个对象的方法,就会得到一个 ClassCastException 异常,因为你还没有这个对象,只有它的序列化代理。
|
||||
序列化代理模式有两个局限性。它不能与可以被客户端拓展的类兼容(详见 19 条)。它也不能与对象图中包含循环的某些类兼容:如果你企图从一个对象的序列化代理的 readResovle 方法内部调用这个对象的方法,就会得到一个 `ClassCastException` 异常,因为你还没有这个对象,只有它的序列化代理。
|
||||
|
||||
最后一点,序列化代理模式所增强的功能和安全性不是没有代价。在我的机器上,通过序列化代理来序列化和反序列化 Period 实例的开销,比使用保护性拷贝增加了 14%。
|
||||
最后一点,序列化代理模式所增强的功能和安全性不是没有代价。在我的机器上,通过序列化代理来序列化和反序列化 `Period` 实例的开销,比使用保护性拷贝增加了 14%。
|
||||
|
||||
总而言之,当你发现必须在一个不能被客户端拓展的类上面编写 readObject 或者 writeObject 方法时,就应该考虑使用序列化代理模式。想要稳健的将带有重要约束条件的对象序列化时,这种模式是最容易的方法。
|
||||
总而言之,当你发现必须在一个不能被客户端拓展的类上面编写 `readObject` 或者 `writeObject` 方法时,就应该考虑使用序列化代理模式。想要稳健的将带有重要约束条件的对象序列化时,这种模式是最容易的方法。
|
Loading…
Reference in New Issue
Block a user