Tips

书中的源代码地址:https://github.com/jbloch/effective-java-3e-source-code

注意,书中的有些代码里方法是基于Java 9 API中的,所以JDK 最好下载 JDK 9以上的版本。

89. 对于实例控制,枚举类型优于READRESOLVE

条目 3描述了单例(Singleton)模式,并给出了以下示例的单例类。 此类限制对其构造方法的访问,以确保只创建一个实例:

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方法,则在新创建的对象反序列化之后,将在该对象上调用该方法。该方法返回的对象引用,代替新创建的对象返回。在该特性的大多数使用中,不保留对新创建对象的引用,因此它立即就有资格进行垃圾收集。

如果Elvis类用于实现Serializable,则以下read-Resolve方法足以保证单例性质:

// 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类攻击。

这种攻击有点复杂,但其基本思想很简单。如果单例包含一个非瞬时状态对象引用属性,则在运行单例的readResolve方法之前,将对该属性的内容进行反序列化。这允许一个精心设计的流在对象引用属性的内容被反序列化时,“窃取”对原来反序列化的单例对象的引用。

下面是它的工作原理。首先,编写一个stealer类,该类具有readResolve方法和一个实例属性,该实例属性引用序列化的单例,其中stealer“隐藏”在其中。在序列化流中,用一个stealer实例替换单例的非瞬时状态属性。现在有了一个循环:单例包含了stealer,而stealer又引用了单例。

因为单例包含stealer,所以当反序列化单例时,stealer的readResolve方法首先运行。因此,当stealer的readResolve方法运行时,它的实例属性仍然引用部分反序列化(且尚未解析)的单例。

stealer的readResolve方法将引用从其实例属性复制到静态属性,以便在readResolve方法运行后访问引用。然后,该方法为其隐藏的属性返回正确类型的值。如果不这样做,当序列化系统试图将stealer引用存储到该属性时,虚拟机会抛出ClassCastException异常。

要使其具体化,请考虑以下有问题的单例:

// 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;
}
}

下面是一个“stealer”类,按照上面的描述构造:

public class ElvisStealer implements Serializable {
static Elvis impersonator;
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" };
}
private static final long serialVersionUID = 0;
}

最后,这是一个丑陋的程序,它反序列化了一个手工制作的流,生成有缺陷单例的两个不同实例。这个程序省略了反序列化方法,因为它与条目88(第354页)的方法相同:

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作为枚举的例子:

// 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的可访问性非常重要。 如果在final类上放置readResolve方法,它应该是私有的。 如果将readResolve方法放在非final类上,则必须仔细考虑其可访问性。 如果它是私有的,则不适用于任何子类。 如果它是包级私有的,它将仅适用于同一包中的子类。 如果它是受保护的或公共的,它将适用于所有不重写它的子类。 如果readResolve方法是受保护或公共访问,并且子类不重写它,则反序列化子类实例将生成一个父类实例,这可能会导致ClassCastException异常。

总而言之,使用枚举类型尽可能强制实例控制不变性。 如果这是不可能的,并且还需要一个类可序列化和实例控制,则必须提供readResolve方法并确保所有类的实例属性都是基本类型,或瞬时状态。

Effective Java 第三版——89. 对于实例控制,枚举类型优于READRESOLVE的更多相关文章

  1. Effective Java 第三版——35. 使用实例属性替代序数

    Tips <Effective Java, Third Edition>一书英文版已经出版,这本书的第二版想必很多人都读过,号称Java四大名著之一,不过第二版2009年出版,到现在已经将 ...

  2. Effective Java 第三版——22. 接口仅用来定义类型

    Tips <Effective Java, Third Edition>一书英文版已经出版,这本书的第二版想必很多人都读过,号称Java四大名著之一,不过第二版2009年出版,到现在已经将 ...

  3. Effective Java 第三版——41.使用标记接口定义类型

    Tips <Effective Java, Third Edition>一书英文版已经出版,这本书的第二版想必很多人都读过,号称Java四大名著之一,不过第二版2009年出版,到现在已经将 ...

  4. 《Effective Java 第三版》目录汇总

    经过反复不断的拖延和坚持,所有条目已经翻译完成,供大家分享学习.时间有限,个别地方翻译得比较仓促,希望有疑虑的地方指出批评改正. 第一章简介 忽略 第二章 创建和销毁对象 1. 考虑使用静态工厂方法替 ...

  5. 《Effective Java 第三版》新条目介绍

    版权声明:本文为博主原创文章,可以随意转载,不过请加上原文链接. https://blog.csdn.net/u014717036/article/details/80588806前言 从去年的3月份 ...

  6. Effective Java 第三版——34. 使用枚举类型替代整型常量

    Tips <Effective Java, Third Edition>一书英文版已经出版,这本书的第二版想必很多人都读过,号称Java四大名著之一,不过第二版2009年出版,到现在已经将 ...

  7. Effective Java 第三版—— 90.考虑序列化代理替代序列化实例

    Tips 书中的源代码地址:https://github.com/jbloch/effective-java-3e-source-code 注意,书中的有些代码里方法是基于Java 9 API中的,所 ...

  8. Effective Java 第三版——3. 使用私有构造方法或枚类实现Singleton属性

    Tips <Effective Java, Third Edition>一书英文版已经出版,这本书的第二版想必很多人都读过,号称Java四大名著之一,不过第二版2009年出版,到现在已经将 ...

  9. Effective Java 第三版——1. 考虑使用静态工厂方法替代构造方法

    Tips <Effective Java, Third Edition>一书英文版已经出版,这本书的第二版想必很多人都读过,号称Java四大名著之一,不过第二版2009年出版,到现在已经将 ...

随机推荐

  1. 【JavaScript】快速入门

    摘抄地址快速入门 No1: JavaScript严格区分大小写 No2: JavaScript不区分整数和浮点数,统一用Number表示 NaN表示Not a Number,当无法计算结果时用NaN表 ...

  2. Editor HDU - 4699 (栈)

    Problem Description   Sample Input 8 I 2 I -1 I 1 Q 3 L D R Q 2   Sample Output 2 3 Hint The followi ...

  3. POJ 2001 Shortest Prefixes 【Trie树】

    <题目链接> 题目大意: 找出能唯一标示一个字符串的最短前缀,如果找不出,就输出该字符串. 解题分析: Trie树的简单应用,对于每个单词的插入,都在相应字符对应的节点 num 值+1 , ...

  4. 多媒体开发(8):调试FFmpeg

    编译FFmpeg得到二进制文件,之后就是对二进制库的调用,这时FFmpeg就像一个黑盒子.作为程序员,难道不想研究一下FFmpeg的具体实现?比如是怎么拿到歌曲信息的.怎么解码的.怎么推流的,等等. ...

  5. linux 学习笔记 groupadd创建组

    1> groupadd -g test2 2>usermod -d /home/test -G test2 test 3>su user 4>groups 注意:root用户才 ...

  6. Linux 系统分级及root权限相关

    单用户模式修改用户密码 救援模式 让我们先来了解一下linux的运行步骤 1. Linux系统引导的顺序掌握Linux系统引导的顺序:BIOSMBRKernelinit其中,BIOS的工作是检查计算机 ...

  7. C# 自定义等待窗口

    private SynchronizationContext syncContext = null; public WaitWindow() { InitializeComponent(); sync ...

  8. python 列表常用操作(二)

    1.tuple 的 unpack a,b = t 2.格式化输出 print('您的输入:{},值为{}',format(a,b)) 3.日期计算 import datetime as dt impo ...

  9. 12.7 Test

    目录 2018.12.7 Test A 序列sequence(迭代加深搜索) B 轰炸bomb(Tarjan DP) C 字符串string(AC自动机 状压DP) 考试代码 A C 2018.12. ...

  10. 英语口语练习系列-C30-生日-年历的周日和月份-如果白昼落进

    词汇-生日-birthday 生日庆祝 birthday celebration birth noun UK /bɜːθ/ US /bɝːθ/ the time when a baby or youn ...