Chapter 11 Serialization

Item 74: Implement Serializable judiciously

让一个类的实例可以被序列化不仅仅是在类的声明中加上“implements Serializable”那么简单。
当你的类implements Serializable并发布出去后,你对这个类的改动的灵活性将会大大降低,它的serialized form成为了它exported API的一部分,如果你不设计一个custom serialized form,而只是接受默认的,那么可以说private的field也成了exported API的一部分。而且,如果你接受了默认的serialized form,然后过一阵又改变了内部实现,那么会导致不兼容性,也就是client如果serialize一个老版本的class的对象,然后再deserialize it with a新版本的class,就会失败(原因比如:因为每一个serializable的class都有一个“serial version UID”,这个UID如果你不显示指定(通过声明一个叫serialVersionUID的static final long的field),那么系统会根据你的类所有的成员和实现的接口会帮你生成一个,这样的话只要你有一点点改动,那么这个UID就变了,导致InvalidClassException(我猜是在把老版本的deserialize成新版本的时候)),除非你用ObjectOutputStream.putFields和ObjectInputStream.readFields,但这样会比较困难。所以你应该在一开始就设计一个high-quality serialized form。
实现Serializable更容易造成bugs或security holes,因为正常的创建一个对象的方式是通过constructor,你肯定会在constructor里检查各种invariants(就是各种你的内部实现需要满足的限制条件),但是你很有可能在deserialization的时候忘了这些,而想靠默认的deserialization肯定不靠谱。
实现Serializable会让测试的负担很大。要新发布一个class的时候,必须测试它是否与老版本能互相serialize和deserialize。所以随着发布版本的增多,测试量就越来越大。作者说用自动化测试也不行,因为还要测试“semantic compatibility”(不明觉厉)。
一般来说,值类型和collection classes应该实现Serializable。Classes representing active entities不应该实现Serializable,比如thread pools。
Classes designed for inheritance很少需要实现Serializable,interfaces很少需要extend Serializable,不然很给子类和“子接口”的实现者很大压力。当然也有例外,比如Throwable,HttpServlet都是Classes designed for inheritance,他们也都实现了Serializable。Throwable是因为这样的话来自remote method invocation (RMI)的异常可以被传过来。HttpServlet是因为这样session state就可以被缓存。注意一点,如果你的这个可以被继承的类有一些invariants,限制某些field不能是初始值(0和null什么的),那么你需要加这个方法:

// readObjectNoData for stateful extendable serializable classes
private void readObjectNoData() throws InvalidObjectException {throw new InvalidObjectException("Stream data required");
}

另外,如果一个designed for inheritance的class如果没有一个parameterless constructor的话,那么继承它的子类就不能被序列化(我估计是因为在子类中的s.defaultReadObject()只会调用父类的无参构造函数,详情见下面的代码)。这时候你可以用下面这个例子中的方法,先放代码再讲解,首先是父类(复习的时候可以选择性跳过,因为感觉很复杂,而且真的这种情况应该很少):

// Nonserializable stateful class allowing serializable subclass
public abstract class AbstractFoo {private int x, y; // Our state// This enum and field are used to track initializationprivate enum State {NEW, INITIALIZING, INITIALIZED};private final AtomicReference<State> init = new AtomicReference<State>(State.NEW);public AbstractFoo(int x, int y) {initialize(x, y);}// This constructor and the following method allow// subclass's readObject method to initialize our state.protected AbstractFoo() {}protected final void initialize(int x, int y) {if (!init.compareAndSet(State.NEW, State.INITIALIZING))throw new IllegalStateException("Already initialized");this.x = x;this.y = y;// 然后应该是验证x和y需要满足的invariantsinit.set(State.INITIALIZED);}// These methods provide access to internal state so it can// be manually serialized by subclass's writeObject method.protected final int getX() {checkInit();return x;}protected final int getY() {checkInit();return y;}// Must call from all public and protected instance methodsprivate void checkInit() {if (init.get() != State.INITIALIZED)throw new IllegalStateException("Uninitialized");}// Remainder omitted
}

然后是子类:

// Serializable subclass of nonserializable stateful class
public class Foo extends AbstractFoo implements Serializable {private void readObject(ObjectInputStream s) throws IOException,ClassNotFoundException {s.defaultReadObject();// Manually deserialize and initialize superclass stateint x = s.readInt();int y = s.readInt();initialize(x, y);}private void writeObject(ObjectOutputStream s) throws IOException {s.defaultWriteObject();// Manually serialize superclass states.writeInt(getX());s.writeInt(getY());}// Constructor does not use the fancy mechanismpublic Foo(int x, int y) {super(x, y);}private static final long serialVersionUID = 1856835860954L;
}

所有AbstractFoo中的public或protected的instance methods都必须先调用checkInit再干别的。这样保证了:如果子类没有“Manually deserialize and initialize superclass state”(看注释)的话,就报错。我一开始在想int x = s.readInt();和int y = s.readInt();的x和y是哪里来的,我推理应该是从用于反序列化的byte流中读到的,但是为什么读到了还要再调用initialize方法?我的推理是:比如你序列化到一个文件,那么别人可以直接修改这个文件,从而修改x和y的值,使x和y本应该满足的条件失效,所以还要再调用initialize,只有这个方法执行完以后才能确保基类中的状态是正确的。
另外,注意那个AtomicReference<State>的field,如果没这玩意儿,那么可能会发生这种情况(虽然我没想出来在什么情况下会出现这种情况,但我选择信了):一个线程正在调用某个实例的initialize,同时另一个线程想“用”这个实例,但是此时可能还没initialize完,所以它还处于inconsistent的状态。而有了那个AtomicReference<State>,只要在那个checkInit方法中检查一下就行了。其他更细节的东西书上也没再说了,但是让我感到不是很清楚的,比如init.get() != State.INITIALIZED这句是原子性的吗?
Inner classes(就是指没有static修饰符的内个)不应该实现Serializable,原因很简单,这种内部类需要一个外部类的实例来构造。但是一个static的inner class是可以实现Serializable的。

Item 75: Consider using a custom serialized form

一个对象默认的serialized form就是:首先一个图(数据结构里面的那个),以这个对象为起点,然后这个对象里面有指向其他对象的fields的话就相当于图里面从这个节点指向另一个节点,然后这个图的物理表示,然后这个物理表示的高效encoding(reasonably efficient encoding of the physical representation of the graph),后面这一串描述确实不太懂,感觉就直接想象成一个图就好了。
如果一个对象的physical representation和他的logical content一样,那么就用默认的serialized form就比较可能是合适的,比如下面这个类:

// Good candidate for default serialized form
public class Name implements Serializable {/*** Last name. Must be non-null.* @serial*/private final String lastName;/*** First name. Must be non-null.* @serial*/private final String firstName;/*** Middle name, or null if there is none.* @serial*/private final String middleName;... // Remainder omitted
}

但你还是要提供一个readObject方法来保证invariants和security。注意上面对private的field也进行了doc comment,因为这些field定义了serialized form,而这属于public API。@serial可以告诉Javadoc utility把这段文档放到一个特别的页面,专门用来说明serialized form。
下面是一个physical representation和logical content完全不同的类:

// Awful candidate for default serialized form
public final class StringList implements Serializable {private int size = 0;private Entry head = null;private static class Entry implements Serializable {String data;Entry next;Entry previous;}
... // Remainder omitted
}

这个类的意思就是存着a list of字符串,对于它的client来说,只知道可以通过它的某些方法来添加或删除某个字符串什么的(上面代码中省略了),但并不知道它内部是用双向链表实现的。所以如果你像上面这样用默认的serialized form,那么the serialized form会非常费力地mirror链表中的每一个entry,和他们之间的links,而且有以下四个缺点:
一.永远地把内部实现跟exported API捆绑在一起了。比如如果以后不想用双向链表表示了,就想用个简简单单的ArrayList,那么你还是不能删掉和Entry有关的代码,即使你根本不需要了(应该是为了兼容老版本)。
二.会消耗过多空间。
三.会消耗过多时间。因为序列化并不知道object graph的拓扑关系,所以它会经历一个昂贵的图的遍历,虽然只要沿着next走是很简单的。
四.容易造成stack overflow。默认的序列化过程会对图执行一次递归的遍历,所以即使包含的节点不算太多,也可能会造成stack overflow。
对于上面的StringList这个类,合理的做法应该是这样:

// StringList with a reasonable custom serialized form
public final class StringList implements Serializable {private transient int size = 0;private transient Entry head = null;// No longer Serializable!private static class Entry {String data;Entry next;Entry previous;}// Appends the specified string to the listpublic final void add(String s) {...}// Implementation omitted  /*** Serialize this {@code StringList} instance.* * @serialData The size of the list (the number of strings it contains) is*             emitted ({@code int}), followed by all of its elements (each*             a {@code String}), in the proper sequence.*/private void writeObject(ObjectOutputStream s) throws IOException {s.defaultWriteObject();s.writeInt(size);// Write out all elements in the proper order.for (Entry e = head; e != null; e = e.next)s.writeObject(e.data);}private void readObject(ObjectInputStream s) throws IOException,ClassNotFoundException {s.defaultReadObject();int numElements = s.readInt();// Read in all elements and insert them in listfor (int i = 0; i < numElements; i++)add((String) s.readObject());}private static final long serialVersionUID = 93248094385L;// Remainder omitted
}

注意上面把所有field都改成transient的了。BTW,如果所有的instance field都是transient的,那么从技术上来说defaultWriteObject和defaultReadObject其实是可以被省略的,但是不建议这么做(为了灵活性和兼容性,比如一个新版本要反序列化成一个旧版本,新增的fields会被忽略,但如果没有defaultReadObject就会报错)。注意,即使writeObject方法是private的,它上面还是有个doc comment,原因和之前Name类是一个道理, @serialData和 @serial的作用一样。然后说说我自己对上面代码的理解,writeObject方法中先s.writeInt(size),意思应该是往byte流中写一个int,然后后面的s.writeObject同理。然后对应地,readObject方法中,先从byte流中读取一个int,然后同理。这样的话是可以做到“发布后也可以改变内部实现”的。
像HashMap就绝对不能用默认的serialized form,我个人推理应该把所有entry对象保存下来,然后反序列化的时候把这些entry对象重新add。
像那些redundant fields都应该被标记为transient,比如那些依赖于特定JVM的field,或者能根据别的field计算出来的field。实际上,当你用custom serialized form的时候,大多数field都应该是transient的。
如果你用的是默认的serialized form,并且也标记了一些transient fields,那么当对象被反序列化的时候,这些fields是默认值。
如果你的类中有方法是同步地读取对象状态,那么你也应该同步你的writeObject方法(我的理解是,因为writeObject方法也会操纵对象状态)。
不管你用什么serialized form(默认也好,customed也好),都应显示声明一个serial version UID。这点在一定程度上会消除不兼容性(见item74),也会避免系统要计算这个值造成的开销。声明它很简单:
private static final long serialVersionUID = randomLongValue ;
如果你写的是一个新的类,那么这个field的值是什么完全无所谓,瞎写都行。但是如果你想改进一个现有的类,把它该进成新版本,然后想让它与改之前的老本本不兼容,只要把serialVersionUID的值变掉就行了(反序列化的时候会InvalidClassException)。

Item 76: Write readObject methods defensively

Item 39中有个叫Period的类,我笔记里没记(为了简单被我改成“Example”了),为了完整性考虑还是写在这里吧:

// Immutable class that uses defensive copying
public final class Period {private final Date start;private final Date end;public Period(Date start, Date end) {this.start = new Date(start.getTime());this.end = new Date(end.getTime());//这里我们的invariant是:end必须大于startif (this.start.compareTo(this.end) > 0)throw new IllegalArgumentException(start + " after " + end);}public Date start () { return new Date(start.getTime()); }public Date end () { return new Date(end.getTime()); }public String toString() { return start + " - " + end; }
}

现在你想让这个类变成是可序列化的,如果你只是在声明中加上“implements Serializable”,那么就会有安全漏洞。
因为readObject方法其实可以理解成一个特殊的constructor,既然在constructor里要检查参数合法性,那么readObject方法也不例外。你可以把readObject方法想成一个接受一个byte stream为参数的构造函数,而这个byte流其实可以人为地构造或修改。所以你完全可以在这个byte流里面把end改成小于start,至于怎么改可以参考Java文档中的byte-stream format。所以,你需要给你的Period类加一个方法:

// readObject method with validity checking
private void readObject(ObjectInputStream s) throws IOException, ClassNotFoundException {s.defaultReadObject();// Check that our invariants are satisfiedif (start.compareTo(end) > 0)throw new InvalidObjectException(start +" after "+ end);
}

(我估计只“implements Serializable”但不加这个方法的话,应该就是只是会调用s.defaultReadObject(),我猜的)然后还有另一种问题就是:可以人为地在Period对象的byte流中加上两个reference,分别指向其内部的start和end,如下所示:

ByteArrayOutputStream bos = new ByteArrayOutputStream();
ObjectOutputStream out = new ObjectOutputStream(bos);// Serialize a valid Period instance
out.writeObject(new Period(new Date(), new Date()));/** Append “rogue reference” for internal Date fields in* Period. For details, see "Java Object Serialization* Specification," Section 6.4.*/
byte[] ref = { 0x71, 0, 0x7e, 0, 5 }; // Ref #5
bos.write(ref); // The start field
ref[4] = 4; // Ref # 4
bos.write(ref); // The end field// Deserialize Period and "stolen" Date references
ObjectInputStream in = new ObjectInputStream(new ByteArrayInputStream(bos.toByteArray()));
period = (Period) in.readObject();
start = (Date) in.readObject();//恭喜你,你获得了一个指向period的private field的引用
end = (Date) in.readObject();

上面用/**/注释起来的那一段代码在byte流中加了两个指向这个Period对象内部的start和end的引用。然后在反序列化的时候,你就可以拥有这两个引用,然后就可以随意修改period内部的start和end。所以,对于那些client不应该拥有的fields,你必须进行defensive copy:

// readObject method with defensive copying and validity checking
private void readObject(ObjectInputStream s)throws IOException, ClassNotFoundException {s.defaultReadObject();// Defensively copy our mutable componentsstart = new Date(start.getTime());end = new Date(end.getTime());// Check that our invariants are satisfiedif (start.compareTo(end) > 0)throw new InvalidObjectException(start +" after "+ end);
}

这样之后,client的“rogue reference”其实指向的就并不是你内部的private fields了。注意,要先defensive copy然后再validity check(item 39解释过),而且不要用Date的clone(因为子类可以乱搞)。注意,上面这种方法需要你把start和end的final修饰符去掉,很不幸你只能这么做。
不要在readObject方法中调用可以override的方法,理由可“不要在constructor里调用可以override的方法”类似,因为overriding的方法可能在子类的状态被初始化(也就是反序列化)之前被调用。
我觉得可以这么理解:byte流是可以任意被人改动的,但是你的readObject方法不能,反序列化一个对象的时候应该先是先进入readObject这个方法(如果有的话),然后s.defaultReadObject()的意思就是根据输入的byte流来初始化对象,然后你可以选择性地进行各种验证。defaultReadObject的文档描述是:Read the non-static and non-transient fields of the current class from this stream。序列化和反序列化一个对象的代码如下所示:

Period p = new Period(new Date(),new Date());
ByteArrayOutputStream os = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(os);
oos.writeObject(p); InputStream is = new ByteArrayInputStream(os.toByteArray());
ObjectInputStream ois = new ObjectInputStream(is);
Period pp = (Period)ois.readObject();

这里的ois.readObject()是唯一一句用于从byte流中反序列化出一个对象的语句。然后设个断点,进入readObject()后发现会继而通过反射进入Period的readObject(ObjectInputStream s)方法,ois.readObject()方法的实现实在是太复杂了,我只能这么推理:byte流中有这个对象的class信息,JVM读到后,到方法区去找这个Period的class object的readObject(ObjectInputStream s)方法。
总结一下:
对于任何private的fields和immutable class的Mutable components,都要进行defensive copy。
检查invariants,不符合就扔InvalidObjectException。
如果整个对象图都需要被验证,就用ObjectInputValidation接口(不知道啥意思,但书上说了就记一下吧)。

Item 77: For instance control, prefer enum types to readResolve

Item 3讲了单例模式,其中有个例子如下:

public class Elvis {public static final Elvis INSTANCE = new Elvis();private Elvis() { ... }
}

但如果你让这个类“implements Serializable”,那么它就不再是个singleton了,因为readObject方法总能返回一个新创建的实例。你可以给上面的类加一个readResolve这个方法来解决问题:

// 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这个类的所有instance fields应该都是transient的(因为反正要被抛弃掉的)。实际上,类型是object reference的field(也就是非primitive type)必须是transient的,否则会有安全漏洞。下面举例说明(复习时可以选择性跳过,我看了N遍才感觉好像看懂了),首先给Elvis加个非transient的field:

private String[] favoriteSongs = { "Hound Dog", "Heartbreak Hotel" };
public void printFavorites() {System.out.println(Arrays.toString(favoriteSongs));
}

其他成员不变(包括readResolve方法),然后定义一个“stealer” class:

public class ElvisStealer implements Serializable {static Elvis impersonator;private Elvis payload;private Object readResolve() {// Save a reference to the "unresolved" Elvis instanceimpersonator = payload;// Return an object of correct type for favorites fieldreturn new String[] { "A Fool Such as I" };}private static final long serialVersionUID = 0;
}

然后,关键来了,我们需要伪造一个Elvis的byte流,让它的favoriteSongs这个field指向一个ElvisStealer的instance(我一开始看到这的时候在想:艹!引用类型不匹配也行?别急往下看),然后当我们反序列化这个byte流的时候,系统发现这个对象包含了一个指向ElvisStealer的instance的field,于是就去反序列化这个ElvisStealer对象,于是乎,ElvisStealer的readResolve方法就会被调用,然后书上说这时候ElvisStealer中的payload指向的就是那个我们正在构造,但还没构造完成的Elvis对象(我选择信了,因为如果这时候再去构造一个Elvis对象的话,就会形成无限递归,那么我只能这么推理:系统先构造了一个field全部为初始值的Elvis对象,然后开始初始化这个对象的field,于是初始化到它的ElvisStealer这个component的时候,发现ElvisStealer又包含一个Elvis的field,于是直接把刚才一开始构造的那个Elvis对象的地址给它,反正早晚那个Elvis对象都会被初始化好的),然后,也是关键,impersonator = payload这句话的意思是把那个我们要偷的引用保存起来(保存到一个static field),以便我们以后还可以用(因为Elvis的readResolve方法,如前所述,会直接抛弃掉被反序列化出来的新对象,所以我们要对这个即将被抛弃的对象保持一个引用),接着给Elvis中的favoriteSongs这个field返回一个类型正确的东西,在这里就是String[]类型的东西,如果你不这么做,那么当虚拟机试图把构造好的ElvisStealer instance的reference存储到这个field中的时候,会抛出ClassCastException。最后反序列化完毕后,ElvisStealer.impersonator保存的就是那个“原本该被抛弃掉的新创建出来的Elvis对象”,它的里面的favoriteSongs是“A Fool Such as I”。
你只要把favoriteSongs的声明加上transient就可以fix这个问题,但是更好的方法是把Elvis变成一个single-element enum type (Item 3):

public enum Elvis {INSTANCE;private String[] favoriteSongs = { "Hound Dog", "Heartbreak Hotel" };public void printFavorites() {System.out.println(Arrays.toString(favoriteSongs));}
}

然后你根本不需要transient和readResolve了,JVM会保证即使反序列化Elvis也只有一个实例(具体为什么书上没说)。但是如果你要写一个serializable的和instance-controlled(比如只能有一个实例,或者只能有两个实例)的类,但是它的实例在编译时不确定,比如要在运行时根据某些参数来构造(这时候我在想如果给enum Elvis定义一个可以接受参数的constructor会怎么样,然后发现!enum类型的constructor只能是private的!也就说明,所有的enum的constant instance都是在编译时就确定的!),就不能用enum了。
如果你的class是final的,那么readResolve方法应该是private的,如果你的类可以被继承,那么readResolve方法的accessibility就需要被考虑,比如如果是protected或public的,那么如果子类没有override它,那么当deserializing a serialized subclass instance的时候,就会返回给你一个superclass instance,就很可能会造成ClassCastException。

Item 78: Consider serialization proxies instead of serialized instances

正如你所见,实现Serializable需要对很多安全性和bug考虑。而有一种技巧可以让这些风险降低很多,这个技巧叫做serialization proxy pattern。首先你要给你的类定义一个private static nested class,代表了你的类的logical state(也就是你应该保证它(这个内部静态类)的默认serialized form是你的类的perfect(logical) serialized form),这个nested class我们就叫它serialization proxy,它只有一个constructor,参数类型是outer class,然后只需要复制data就行了,不需要任何defensive copy或invariant检查。outer class和这个serialization proxy都需要实现Serializable。比如,item76中最开始的Period类,你可以给它定义一个serialization proxy:

// Serialization proxy for Period class
private static class SerializationProxy implements Serializable {private final Date start;private final Date end;SerializationProxy(Period p) {this.start = p.start;this.end = p.end;}private static final long serialVersionUID =234098243823485285L; // Any number will do (Item 75)
}

然后给Period加一个方法:

// writeReplace method for the serialization proxy pattern
private Object writeReplace() {return new SerializationProxy(this);
}

下面是来自java文档关于这个方法的说明:

Serializable classes that need to designate an alternative object to be used when writing an object to the stream should implement this special method with the exact signature: ANY-ACCESS-MODIFIER Object writeReplace() throws ObjectStreamException;

也就是在把对象写入byte流之前,可以用这个方法,把将要被写入byte流的对象换成另一个对象。这里就相当于把一个Period对象转换成了它的一个SerializationProxy对象。
为了使黑客攻击失败,我们给Period再加一个方法:

// readObject method for the serialization proxy pattern
private void readObject(ObjectInputStream stream) throws InvalidObjectException {throw new InvalidObjectException("Proxy required");
}

因为我们根本不需要反序列化出一个Period对象(往下看就知道了)。最后,给SerializationProxy加一个方法:

// readResolve method for Period.SerializationProxy
private Object readResolve() {return new Period(start, end); // Uses public constructor
}

这个方法相当于在反序列化的时候,把serialization proxy对象转回成Period对象。这个模式的精妙之处就在这:因为用的是Period的一个public constructor(也可以是static factories什么的),这样你就不用再花力气去检查invariants了,因为这样创建出来的对象,和你通过普通的constructor创建出来的对象,从某种程度上来说是差不多的。而且你以后如果新加了一些invariants,也可以确保反序列化创建的对象也满足这些invariants。
我个人理解:序列化的时候会把一个Period对象对应的SerializationProxy对象变成byte流,反序列化的时候,因为JVM看到这个byte流的class信息是SerializationProxy,所以调用SerializationProxy这个类中的readResolve方法,从而得到了一个Period对象。
还记得EnumSet(Item 32)吗?从client的角度看,得到的都是EnumSet instances,但实际上可能返回两种具体的子类:RegularEnumSet(不大于64个enum元素)和JumboEnumSet(大于64个enum元素)。现在假设有这么一种情况:你的一个Enum类有60中元素,然后你现在有一个这个Enum类得到的EnumSet对象,并且把它序列化了,然后你又给你的Enum类加了5个元素,然后这时候你又把刚才序列化后的byte流反序列化出来得到的EnumSet,会是一个JumboEnumSet吗?答案是肯定的。因为EnumSet正是用了serialization proxy pattern:

// EnumSet's serialization proxy
private static class SerializationProxy <E extends Enum<E>> implements Serializable {// The element type of this enum set.private final Class<E> elementType;// The elements contained in this enum set.private final Enum[] elements;SerializationProxy(EnumSet<E> set) {elementType = set.elementType;elements = set.toArray(EMPTY_ENUM_ARRAY); // (Item 43)}private Object readResolve() {//Creates an empty enum set with the specified element typeEnumSet<E> result = EnumSet.noneOf(elementType);for (Enum e : elements)result.add((E)e);return result;}private static final long serialVersionUID = 362491234563181265L;
}

上面readResolve的方法实现是关键,因为你的byte流中其实只含有elementType和elements这两个field,当反序列化出来的时候,又根据这两个field重新构建了一个EnumSet对象,当然会根据现在的Enum元素个数来构建了!
serialization proxy pattern有两点限制:1.不适用于可以被clients继承的类(我猜是因为子类如果新加了一些field,必须修改SerializationProxy中对应的field才行)。2.不适用于“object graphs contain circularities”的类(我的理解就是:A a = new A();B b = new B(); a.b = b;b.a = a;),至于为什么我写了一个测试代码:

public class Program {public static void main(String[] args) throws IOException, ClassNotFoundException {B b = new B();A a = new A(b);b.setA(a);//如果把这句注释掉就不会报错ByteArrayOutputStream os = new ByteArrayOutputStream();ObjectOutputStream oos = new ObjectOutputStream(os);oos.writeObject(a);InputStream is = new ByteArrayInputStream(os.toByteArray());ObjectInputStream ois = new ObjectInputStream(is);a = (A) ois.readObject();}
}final class A implements Serializable {private B b;public A(B b) {this.b = b;}private static class SerializationProxy implements Serializable {private B b;SerializationProxy(A a) {this.b = a.b;}private Object readResolve() {return new A(b); }private static final long serialVersionUID = 234098243823485285L;}private Object writeReplace() {return new SerializationProxy(this);}private void readObject(ObjectInputStream stream) throws InvalidObjectException {throw new InvalidObjectException("Proxy required");}
}class B implements Serializable {private A a;public void setA(A a) {this.a = a;}
}

我是这么想的:反序列化的时候,一读byte流里面的信息,发现要反序列化一个SerializationProxy对象,OK直接new一个field都是默认值的,然后要初始化这个对象里面的field,发现b这个field在byte流里面指向的是一个B对象,那我们再new一个B对象,然后要初始化b对象的field,发现byte流里面B对象里的a这个field指向的是一个A对象,这里问题来了,我只能这么推测了:就算是A里面有writeReplace()方法,估计也会在byte流里记录A这个class的信息,然后让反序列化这个class的人以为他在反序列化一个A对象,然后如果发现又有field要指向当前这个正在构建的对象的时候,就直接把这个正在构建的对象引用给它,于是乎抛出“java.lang.ClassCastException: cannot assign instance of A\(SerializationProxy to field B.a of type A in instance of B”,意思就是不能把你正在构建的这个实际是A\)SerializationProxy类型(但你以为是A类型)的对象的引用传给A类型的field。我只能这么推理了,不然解释不通为什么会抛出ClassCastException而不是InvalidObjectException。但也许我的推理是错的。
另外,serialization proxy pattern的性能会差一点。

转载于:https://www.cnblogs.com/raytheweak/p/7258390.html

《Effective Java》读书笔记 - 11.序列化相关推荐

  1. 《Effective Java读书笔记》--序列化

    2019独角兽企业重金招聘Python工程师标准>>> 谨慎地实现Serializable 实现Serializable有以下几点风险 实现Serializable的类一旦发布,则& ...

  2. Effective Java读书笔记(二)

    Effective Java 读书笔记 (二) 创建和销毁对象 遇到多个构造器参数时要考虑使用构建器 创建和销毁对象 何时以及如何创建对象? 何时以及如何避免创建对象? 如何确保它们能够适时地销毁? ...

  3. Effective Java 读书笔记(七):通用程序设计

    Effective Java 读书笔记七通用程序设计 将局部变量的作用域最小化 for-each 循环优于传统的 for 循环 了解和使用类库 如果需要精确的答案请避免使用 float 和 doubl ...

  4. Effective Java读书笔记八:序列化(74-78)

    第74条:谨慎地实现Serializable接口 对象序列化API,它提供了一个框架,用来将对象编码成字节流,并从字节流编码中重新构建对象.将一个对象编码成一个字节流,称作将该对象序列化,相反的处理过 ...

  5. Effective Java读书笔记完结啦

    Effective Java是一本经典的书, 很实用的Java进阶读物, 提供了各个方面的best practices. 最近终于做完了Effective Java的读书笔记, 发布出来与大家共享. ...

  6. Effective Java 读书笔记(一)

    前言: 开个新的坑位,<effective java>的读书笔记,之后有时间会陆陆续续的更新,读这本书真的感触满多,item01和item02就已经在公司的项目代码中看到过了.今天这篇主要 ...

  7. Effective Java读书笔记六:方法

    第38条:检查参数的有效性 绝大多数方法和构造器对于传递给它们的参数值都会有些限制.比如,索引值必须大于等于0,且不能超过其最大值,对象不能为null等.这样就可以在导致错误的源头将错误捕获,从而避免 ...

  8. Effective Java读书笔记五:异常

    第57条:只针对异常的情况才使用异常 异常是为了在异常情况下使用而设计的,不要将它们用于普通的控制流,也不要编写迫使它们这么做的API. 下面部分来自:异常 如果finally块中出现了异常没有捕获或 ...

  9. Effective Java读书笔记三:创建和销毁对象

    第1条:考虑用静态工厂方法代替构造器 对于类而言,为了让客服端获得它的一个实例最常用的的一个方法就是提供一个公有的构造器.还有一种方法,类可以提供一个公有的静态工厂方法(static factory ...

  10. Effective Java读书笔记二:枚举和注解

    第30条:用enum代替int常量 当需要一组固定常量的时候,应该使用enum代替int常量,除了对于手机登资源有限的设备应该酌情考虑enum的性能弱势之外. 第31条:用实例域代替序数 枚举的ord ...

最新文章

  1. 【机器学习】机器学习中缺失值处理方法大全(附代码)
  2. 十大排序算法之冒泡排序
  3. 怎么在matlab中图像中外接矩形,Matlab 最小外接矩形
  4. iframe给php传值,向iframe传递参数
  5. jquery for循环_前端基础入门五(掌握jQuery的常用api,实现动态效果)
  6. 【Foreign】字串变化 [DP]
  7. 递归处理汉诺塔问题(c++/python)
  8. Python_作业_Day_1
  9. Builder(生成器)
  10. qPCR引物设计经验教程
  11. 如何设置打印机双面打印?
  12. html如何控制plc,PLC系统是如何控制数控机床的
  13. pandas中DataFrame如何检测重复值
  14. Adobe Illustrator【印前角线X2.0】脚本源码
  15. 仙剑5手游服务器维护,仙剑奇侠传手游5月27日例行维护与活动公告
  16. php 微信 防刷票,细数那些防止微信刷票的设置
  17. 人人都要懂的代码重构
  18. 服务器2012系统崩溃,意外的Windows服务器2012 R2上的WCF服务崩溃
  19. matlab 雷达拼图,(完整版)SAR合成孔径雷达图像点目标仿真报告(附matlab代码)
  20. 设置(settings)

热门文章

  1. 【STM32】定时器TIM触发ADC采样,DMA搬运到内存(超详细讲解)
  2. L1 - Learn 8 Phrasal Verbs for opening: pop open, peel off, flip up…
  3. 高德地图-添加一个或多个覆盖物
  4. 虚拟页式存储管理——页面置换算法及其影响的缺页中断率
  5. html如何注释文字,css怎么注释?
  6. 【Python】根据汽车品牌列表及链接地址分别获取对应子品牌及车系数据列表
  7. 【python之父】:从他的经历中我又重拾对编程的热爱,程序员也不是一直写代码就没有出路
  8. ubuntu18.04 ROS Basler相机详细步骤
  9. 深度广度并举,AWS容器服务再推利器!
  10. Genexus第5篇-WebPanel