Tips
书中的源代码地址:https://github.com/jbloch/effective-java-3e-source-code
注意,书中的有些代码里方法是基于Java 9 API中的,因此JDK 最好下载 JDK 9以上的版本。java
条目 3描述了单例(Singleton)模式,并给出了如下示例的单例类。 此类限制对其构造方法的访问,以确保只建立一个实例:git
public class Elvis { public static final Elvis INSTANCE = new Elvis(); private Elvis() { ... } public void leaveTheBuilding() { ... } }
如条目 3所述,若是将implements Serializable
添加到类的声明中,则此类将再也不是单例。 类是否使用默认的序列化形式或自定义序列化形式(条目 87)并不重要,该类是否提供显式的readObject方法(条目 88项)也可有可无。 任何readObject方法,不管是显式方法仍是默认方法,都会返回一个新建立的实例,该实例与在类初始化时建立的实例不一样。github
readResolve特性容许你用另外一个实例替换readObject方法 [Serialization, 3.7]建立的实例。若是正在反序列化的对象的类,使用正确的声明定义了readResolve方法,则在新建立的对象反序列化以后,将在该对象上调用该方法。该方法返回的对象引用,代替新建立的对象返回。在该特性的大多数使用中,不保留对新建立对象的引用,所以它当即就有资格进行垃圾收集。app
若是Elvis
类用于实现Serializable,则如下read-Resolve方法足以保证单例性质:ui
// 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方法以前,将对该属性的内容进行反序列化。这容许一个精心设计的流在对象引用属性的内容被反序列化时,“窃取”对原来反序列化的单例对象的引用。code
下面是它的工做原理。首先,编写一个stealer
类,该类具备readResolve方法和一个实例属性,该实例属性引用序列化的单例,其中stealer
“隐藏”在其中。在序列化流中,用一个stealer
实例替换单例的非瞬时状态属性。如今有了一个循环:单例包含了stealer
,而stealer
又引用了单例。orm
由于单例包含stealer
,因此当反序列化单例时,stealer
的readResolve方法首先运行。所以,当stealer
的readResolve方法运行时,它的实例属性仍然引用部分反序列化(且还没有解析)的单例。对象
stealer
的readResolve方法将引用从其实例属性复制到静态属性,以便在readResolve方法运行后访问引用。而后,该方法为其隐藏的属性返回正确类型的值。若是不这样作,当序列化系统试图将stealer
引用存储到该属性时,虚拟机会抛出ClassCastException异常。blog
要使其具体化,请考虑如下有问题的单例:
// 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方法并确保全部类的实例属性都是基本类型,或瞬时状态。