effective-java-3rd-chinese/docs/notes/89. 对于实例控制,枚举类型优于 readResolve.md
sjsdfg c95d1e401c update markdown code statement
```Java -> ```java
2019-03-14 14:02:05 +08:00

134 lines
8.5 KiB
Markdown
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 89. 对于实例控制,枚举类型优于 readResolve
  第 3 条讲述了 Singletion单例模式并且给出了以下这个 Singletion 示例。这个类限制了对其构造器的访问,以确保永远只创建一个实例。
```java
public class Elvis {
public static final Elvis INSTANCE = new Elvis();
private Elvis() { ... }
public void leaveTheBuilding() { ... }
}
```
  正如在第 3 条中所提到的,如果在这个类上面增加 `implements Serializable` 的字样,它就不是一个单例。无论该类使用了默认的序列化形式,还是自定义的序列化形式(详见 87 条),都没有关系;也跟它是否使用了显式的 `readObject`(详见 88 条)无关。任何一个 `readObject` 方法,不管是显式的还是默认的,都会返回一个新建的实例,这个新建的实例不同于类初始化时创建的实例。
  `readResolve` 特性允许你用 `readObject` 创建的实例代替另外一个实例[Serialization, 3.7]。对于一个正在被反序列化的对象,如果它的类定义了一个 `readResolve` 方法,并且具备正确的声明,那么在反序列化之后,新建对象上的 `readResolve` 方法就会被调用。然后,该方法返回的对象引用将会被回收,取代新建的对象。这个特性在绝大多数用法中,指向新建对象的引用不会再被保留,因此成为垃圾回收的对象。
  如果 Elvis 类要实现 `Serializable` 接口,下面的 `readResolve` 方法就足以保证它的单例属性:
```java
// readResolve for instance control - you can do better!
private Object readResolve() {
// Return the one true Elvis and let the garbage collector
// take care of the Elvis impersonator.
return INSTANCE;
}
```
  该方法忽略了被反序列化的对象,只返回类初始化创建的那个特殊的 `Elvis` 实例。因此 `Elvis` 实例的序列化形式不应该包含任何实际的数据;所有的实例字段都应该被声明为 `transient`。事实上,**如果依赖 `readResolve` 进行实例控制,带有对象引用类型的所有实例字段都必须声明为 `transient`。** 否则,那种破釜沉舟式的攻击者,就有可能在 `readResolve` 方法运行之前,保护指向反序列化对象的引用,采用的方式类似于在第 88 条中提到的 `MutablePeriod` 攻击。
  这种攻击有点复杂,但是背后的思想十分简单。如果单例包含一个非 `transient` 的对象引用字段,这个字段的内容就可以在单例的 `readResolve` 方法之前被反序列化。当对象引用字段的内容被反序列化时,它就允许一个精心制作的流“盗用”指向最初被反序列化的单例对象引用。
  以下是它更详细的工作原理。首先编写一个“盗用者”类,它既有 `readResolve` 方法,又有实例字段,实例字段指向被序列化的单例的引用,“盗用者”就“潜伏”在其中。在序列化流中,用“盗用者”的 `readResolve` 方法运行时,它的实例字段仍然引用部分反序列化(并且还没有被解析)的 Singletion。
  “盗用者”的 `readResolve` 方法从它的实例字段中将引用复制到静态字段中,以便该引用可以在 `readResolve` 方法运行之后被访问到。然后这个方法为它所藏身的那个域返回一个正确的类型值。如果没有这么做,当序列化系统试着将“盗用者”引用保存到这个字段时,虚拟机就会抛出 `ClassCastException`
  为了更具体的说明这一点,我们以如下这个单例模式为例:
```java
// Broken singleton - has nontransient object reference field!
public class Elvis implements Serializable {
public static final Elvis INSTANCE = new Elvis();
private Elvis() { }
private String[] favoriteSongs =
{ "Hound Dog", "Heartbreak Hotel" };
public void printFavorites() {
System.out.println(Arrays.toString(favoriteSongs));
}
private Object readResolve() {
return INSTANCE;
}
}
```
  如下“盗用者”类,是根据以上描述构造的:
```java
public class ElvisStealer implements Serializable {
static Elvis impersonator;
private static final long serialVersionUID = 0;
private Elvis payload;
private Object readResolve() {
// Save a reference to the "unresolved" Elvis instance
impersonator = payload;
// Return object of correct type for favoriteSongs field
return new String[] { "A Fool Such as I" };
}
}
```
  下面是一个不完整的程序,它反序列化一个手工制作的流,为那个有缺陷的单例产生两个截然不同的实例。这个程序省略了反序列化方法,因为它与第 88 条一样。
```java
public class ElvisImpersonator {
// Byte stream couldn't have come from a real Elvis instance!
private static final byte[] serializedForm = {
(byte) 0xac, (byte) 0xed, 0x00, 0x05, 0x73, 0x72, 0x00, 0x05, 0x45,
0x6c, 0x76, 0x69, 0x73, (byte) 0x84, (byte) 0xe6, (byte) 0x93, 0x33,
(byte) 0xc3, (byte) 0xf4, (byte) 0x8b, 0x32, 0x02, 0x00, 0x01, 0x4c,
0x00, 0x0d, 0x66, 0x61, 0x76, 0x6f, 0x72, 0x69, 0x74, 0x65, 0x53,
0x6f, 0x6e, 0x67, 0x73, 0x74, 0x00, 0x12, 0x4c, 0x6a, 0x61, 0x76,
0x61, 0x2f, 0x6c, 0x61, 0x6e, 0x67, 0x2f, 0x4f, 0x62, 0x6a, 0x65,
0x63, 0x74, 0x3b, 0x78, 0x70, 0x73, 0x72, 0x00, 0x0c, 0x45, 0x6c,
0x76, 0x69, 0x73, 0x53, 0x74, 0x65, 0x61, 0x6c, 0x65, 0x72, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0x00, 0x01, 0x4c,
0x00, 0x07, 0x70, 0x61, 0x79, 0x6c, 0x6f, 0x61, 0x64, 0x74, 0x00,
0x07, 0x4c, 0x45, 0x6c, 0x76, 0x69, 0x73, 0x3b, 0x78, 0x70, 0x71,
0x00, 0x7e, 0x00, 0x02
};
public static void main(String[] args) {
// Initializes ElvisStealer.impersonator and returns
// the real Elvis (which is Elvis.INSTANCE)
Elvis elvis = (Elvis) deserialize(serializedForm);
Elvis impersonator = ElvisStealer.impersonator;
elvis.printFavorites();
impersonator.printFavorites();
}
}
```
  这个程序会产生如下的输出,最终证明可以创建两个截然不同的 `Elvis` 实例(包含两种不同的音乐品味):
```
[Hound Dog, Heartbreak Hotel]
[A Fool Such as I]
```
  通过将 `favoriteSongs` 字段声明为 `transient`,可以修复这个问题,但是最好把 `Elvis` 做成一个单元素的枚举类型(详见第 3 条)。就如 `ElvisStealer` 攻击所示范的,用 `readResolve` 方法防止“临时”被反序列化的实例收到攻击者的访问,这种方法十分脆弱需要万分谨慎。
  如果将一个可序列化的实例受控的类编写为枚举Java 就可以绝对保证出了所声明的常量之外,不会有其他实例,除非攻击者恶意的使用了享受特权的方法。如 `AccessibleObject.setAccessible`。能够做到这一点的任何一位攻击者,已经拥有了足够的特权来执行任意的本地代码,后果不堪设想。将 Elvis 写成枚举的例子如下所示:
```java
// Enum singleton - the preferred approach
public enum Elvis {
INSTANCE;
private String[] favoriteSongs = { "Hound Dog", "Heartbreak Hotel" };
public void printFavorites() {
System.out.println(Arrays.toString(favoriteSongs));
}
}
```
  用 readResolve 进行实例控制并不过时。如果必须编写可序列化的实力受控的类,在编译时还不知道它的实例,你就无法将类表示成为一个枚举类型。
  **readResolve 的可访问性accessibility十分重要。** 如果把 `readResolve` 方法放在一个 `final` 类上面,它应该是私有的。如果把 `readResolve` 方法放在一个非 `final` 类上,就必须认真考虑它的的访问性。如果它是私有的,就不适用于任何一个子类。如果它是包级私有的,就适用于同一个包内的子类。如果它是受保护的或者是公开的,并且子类没有覆盖它,对序列化的子类进行反序列化,就会产生一个超类实例,这样可能会导致 `ClassCastException` 异常。
  总而言之,应该尽可能的使用枚举类型来实施实例控制的约束条件。如果做不到,同时又需要一个即可序列化又可以实例受控的类,就必须提供一个 `readResolve` 方法,并确保该类的所有实例化字段都被基本类型,或者是
`transient` 的。