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. js获取http请求响应头信息

    var req = new XMLHttpRequest(); req.open('GET', document.location, false); req.send(null); var heade ...

  2. XamarinSQLite教程添加列

    XamarinSQLite教程添加列 如果开发者想要在现有的表中添加列,并不需要删除重新创建数据表,只需要修改数据表.操作步骤如下. (1)右击需要添加列的表,单击Add column…(beta)命 ...

  3. Vue实现用户自定义上传头像裁剪

    使用技术: vue.js2.0.cropperjs.canvas <template>   <div id="app">     <div id=&q ...

  4. Redis自学笔记:2.准备

    第2章:准备 '纸上得来终觉浅,绝知此事要躬行'--陆游 2.2启动和停止redis 表2-1 redis可执行文件说明 文件名 说明 redis- server redis服务器 redis-cli ...

  5. sql之Replace

    update [ConfigDb].[dbo].[RuleParams] set RuleName=REPLACE(RuleName,'业务','') where (RuleId>358 or ...

  6. 统一各浏览器CSS 样式——CSS Reset

    html, body, div, span, applet, object, iframe, h1, h2, h3, h4, h5, h6, p, blockquote, pre, a, abbr, ...

  7. Debian stretch更换国内源

    在debian图形化安装过程中就可以选择网络镜像的位置 据说电信用清华的源快,移动的用网易源快 备份源配置文件: cp /etc/apt/sources.list /etc/apt/sources.l ...

  8. [P3625][APIO2009]采油区域 (前缀和)

    这道题用二维前缀和可以做 难度还不算高,细节需要注意 调试了很久…… 主要是细节太多了 #include<bits/stdc++.h> using namespace std; #defi ...

  9. Java 不变模式

    在阎宏博士的<JAVA与模式>一书中开头是这样描述不变(Immutable)模式的:一个对象的状态在对象被创建之后就不再变化,这就是所谓的不变模式. 不变模式的结构 不变模式可增强对象的健 ...

  10. List集合的总结和应用场景的介绍

    1.List的整体介绍 List 是一个接口,它继承于Collection的接口,它代表着有序的队列.list的实现类对象中每一个元素都有一个索引值,能够按照索引值进行元素查找. AbstractLi ...