zoukankan      html  css  js  c++  java
  • 《Effective Java》读书笔记

    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的性能会差一点。

  • 相关阅读:
    什么是多线程中的上下文切换?
    java 中有几种方法可以实现一个线程?
    什么叫线程安全?servlet 是线程安全吗?
    什么是线程调度器(Thread Scheduler)和时间分片(Time Slicing )?
    一个线程运行时发生异常会怎样?
    我的样式
    springboot+dynamic多数据源配置
    mybatis plus条件拼接
    springboot配置虚拟路径
    springboot+mybatis+Druid配置多数据源(mysql+postgre)
  • 原文地址:https://www.cnblogs.com/raytheweak/p/7258390.html
Copyright © 2011-2022 走看看