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

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 initialization
    private 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需要满足的invariants
        init.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 methods
    private 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 state
        int x = s.readInt();
        int y = s.readInt();
        initialize(x, y);
    }

    private void writeObject(ObjectOutputStream s) throws IOException {
        s.defaultWriteObject();

        // Manually serialize superclass state
        s.writeInt(getX());
        s.writeInt(getY());
    }

    // Constructor does not use the fancy mechanism
    public 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 list
    public 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 list
        for (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必须大于start
        if (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 satisfied
    if (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 components
    start = new Date(start.getTime());
    end = new Date(end.getTime());
    // Check that our invariants are satisfied
    if (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 instance
        impersonator = payload;

        // Return an object of correct type for favorites field
        return 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 type
        EnumSet<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

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值