序列化

来源:互联网 发布:c语言写学生管理系统 编辑:程序博客网 时间:2024/06/04 17:41

本章关注对象序列化API,它提供了一个框架,用来将对象编码成字节流,并从字节流编码中重新构建对象。 相反的处理过程是反序列化deserializing。一旦对象被序列化后,它的编码就可以从一台正在运行的虚拟机被传递到另一台虚拟机上,或者被存储到磁盘上,供以后反序列化时用。序列化技术为远程通信提供了标准的线路级对象表示法,也为JavaBean组件结构提供了标准的持久化数据格式。

第七十四条、谨慎地实现Serializable接口

  1. 要想使一个类的实例可以被序列化,只要在它的声明中加入implements Serializable字样即可,但实际情况可能要复杂得多,虽然使一个类可被实例化的直接开销非常低,但是为了序列化而付出的长期开销往往是实实在在的。
    实现Serializable接口的代价:

    • 最大的代价是:一旦一个类被发布,就大大降低了“改变这个类的实现”的灵活性

      如果一个类实现了Serializable接口,它的字节流编码就变成了它导出的API的一部分,一旦这个类被广泛使用,往往永远支持这个序列化形式。如果你接受了默认的序列化形式,并且以后又要修改这个类的内部表示法,可能会导致序列化形式的不兼容。

      序列化会使类的演变受到限制,这种限制的一个例子与流的唯一标识符有关,通常它也被称为序列版本UID(serialVersionUID),每个可序列化的类都有一个唯一标识号与它相关联。如果你没有显式地指定该标识号(private static final long serialVersionUID = ...),系统就会自动地在运行时产生该标识号。这个自动产生的值会受到类名称、它实现的接口名称、以及所有的公有和受保护的成员的名称所影响。因此,如果你没有声明一个显式的序列版本UID,兼容性会遭到破坏。

    • 第二个代价是:它增加了出现Bug和安全漏洞的可能性。通常情况下,对象是通过构造器来创建的;序列化机制是一种语言之外的对象创建机制。反序列化机制是一个隐藏的构造器,依靠默认的反序列化机制,很容易使对象的约束关系遭到破坏,以及遭受非法访问

    • 第三个代价:随着类发行新的版本,相关的测试负担也增加了。

  2. 实现Serializable接口提供了实在的好处:

    如果一个类将要加入到某个框架中,并且该框架依赖于序列化来实现对象传输或者持久化,对于这个类来说,实现Serializable接口就非常有必要。

  3. 为了继承而设计的类应该尽可能少地去实现Serializable接口,用户的接口也应该尽可能少地实现。如果违反了这条规则,扩展这个类或者实现这个接口的程序员就会背上沉重的负担。但是有例外:Throwable(RMI的异常可以从服务器端传到客户端)类,Component(GUI可以被发送、保存和恢复)和HttpServlet抽象类(会话状态可以被缓存)。

  4. 内部类(inner class)不应该实现Serializable,它们使用编译器产生的合成域来指向外围实例的引用,以及保存来自外围作用域的局部变量的值。


第七十五条、考虑使用自定义的序列化形式

  1. 如果没有先认真考虑默认的序列化形式是否合适,则不要贸然接受。接受默认的序列化形式是一个非常重要的决定。需要从灵活性、性能和正确性多个角度对这种编码形式进行考察。

  2. 默认的序列化形式描述了该对象内部所包含的数据,以及每一个可以从这个对象到达的其他对象的内部数据。它也描述了所有这些对象被链接起来后的拓扑结构。对于一个对象来说,理想的序列化形式应该只包含该对象所表示的逻辑数据,而逻辑数据与物理表示法应该是各自独立的。如果一个对象的物理表示法等同于它的逻辑内容,可能就适合于使用默认的序列化形式。即使你确定了默认的序列化形式是合适的,通常还必须提供一个readObject方法以保证约束关系和安全性。

  3. 当一个对象的物理表示法与它的逻辑数据内容有实质性区别的时候,使用默认的序列化形式会有以下四个缺点:

    • 它使这个类的导出API永远地束缚在该类的内部表示法上;
    • 它会消耗过多的空间;
    • 它会消耗过多的时间;
    • 它会引起栈溢出。
  4. 合理的序列化形式实例:

    writeObject方法的首要任务是调用defaultWriteObject,readObject的首要方法是调用defaultReadObject。无论你是否使用默认的序列化形式,当defaultWriteObject方法被调用的时候,每个未被标记为transient的实例域都会被序列化。在决定将一个域做成非transient的之前,请一定要确信它的值将是该对象逻辑状态的一部分。如果要自定义序列化形式,大多数或者所有的实例域都应该标记为transient。

        import java.io.IOException;    import java.io.ObjectInputStream;    import java.io.ObjectOutputStream;    import java.io.Serializable;    /**     * Created by laneruan on 2017/8/3.     * 一个自定义的序列化形式实例     */    public class StringListSerialization implements Serializable {        private transient int size = 0;     //瞬时的        private transient Entry head = null;        private static class Entry{            String data;            Entry next;            Entry previous;        }        public final void add(String s){        }        private void writeObject(ObjectOutputStream s)            throws IOException{            s.defaultWriteObject();            s.writeInt(size);            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();            for(int i = 0;i<numElements;i++){                add((String)s.readObject());            }        }    }
  5. 对于有些对象的约束关系要依赖于特定的实现细节,用默认的序列化形式会破坏其约束关系。比如考虑散列表的情形。

  6. 如果正在使用默认的序列化形式,并且把一个或者多个域标记为transient,则需要记住:当一个实例被反序列化的时候,这些域将被初始化为它们的默认值(default value)

    对于对象引用域,默认值为null。

    对于数值基本域,默认值为0。

    对于boolean域,默认值为false。

    如果这些值不能被任何transient域所接受,则必须要提供readObject方法,它首先调用defaultreadObject方法,再把这些transient域恢复为可接受的值。

  7. 无论是否使用默认形式,都需要注意:

    • 如果在读取整个对象状态的任何其他方法上强制任何同步,则也必须在对象序列化上强制这种同步。如果把同步放在writeObject方法上,就必须确保它遵守与其他动作相同的锁排列约束条件。

          private synchronized void writeObject(ObjectOutputStream s)        throws IOException{        s.defaultWriteObject();    }
    • 要为自己编写的每个可序列的类声明一个显式的序列版本UID。这样可以避免序列版本UID成为潜在的不兼容根源,且带来小小的性能优势。
      private static final long serialVersionUID = randomLongValue;

第七十六条、保护性地编写readObject方法

  1. 每当你编写readObject方法的时候,都要这样想:你正在编写一个公有的构造器,无论给它传递什么样的字节流,它都必须产生一个有效的实例,不要假设这个字节流一定代表着一个真正被序列化过的实例,下面给出一些指导性建议,有助于编写出更加健壮的readObject方法:

    • 对于对象引用域必须保持为私有的域,要保护性地拷贝这个域中的每个对象,不可变类的可变组件就属于这一类别;

    • 对于任何约束条件,如果检查失败,则抛出一个InvalidObjectException,这些检查动作应该跟在所有的保护性拷贝后面;

    • 如果整个对象图在被反序列化之后必须进行验证,就应该使用ObjectInputValidation接口;

    • 无论是直接还是间接方式,都不要调用类中任何可被覆盖的方法。


第七十七条、对于实例控制,枚举类型优先于readResolve

  1. 之前讲过的Singleton模式,一般这种类限制了构造器的访问,以确保永远只创建一个实例。但是,如果这种类的声明加上了implements Serializable,就不再是Singleton。任何一个readObject方法,都会返回一个新建的实例,这个新建的实例不同于该类初始化时创建的实例。

  2. readSolve特性允许你用readObject创建的实例代替另一个实例,对于一个正在被反序列化的对象,如果它的类定义了一个readSolve方法,并且具备正确的声明,那么反序列化之后,新建对象上的readSolve方法就会被调用,然后,该方法返回的对象引用将被返回,取代新建的对象。如果依赖readSolve进行实例控制,带有引用类型的所有实例域则都必须声明为transient的

  3. 如果将一个可序列化的实例受控类编写成枚举,就可以绝对保证除了所声明的常量之外,不会有别的实例(JVM保证的)

        import java.io.Serializable;    import java.util.Arrays;    /**     * Created by laneruan on 2017/8/3.     */    public class singletonReadSolve implements Serializable {        public static final singletonReadSolve INSTANCE = new singletonReadSolve();        private singletonReadSolve(){}        //这个方法足以保证Singleton属性        private String[] favoriteSongs = {"Hound dog","Heartbreak Hotel"};        public void printFavorites(){            System.out.println(Arrays.toString(favoriteSongs));        }        private Object readResolve(){            return INSTANCE;        }        //用enum类型进行实例控制        public enum singletonEnum{            INSTANCE;            private String[] favoriteSongs = {"Hound dog","Heartbreak Hotel"};            public void printFavorites(){                System.out.println(Arrays.toString(favoriteSongs));            }        }    }
  4. readResolve的可访问性很重要,如果把它放在一个final类上,就应该是私有的,如果放在非final类上,就必须认真考虑它的访问性。

  5. 总结:应该尽可能使用枚举类型来实施实例控制的约束条件,如果做不到,同时有需要一个既可以序列化优势实例受控的类,就必须提供一个readResolve方法,并确保该类的所有实例域都是基本类型或者是transient的。


第七十八条、考虑用序列化代理代替序列化实例

  1. 序列化代理模式(serialization proxy pattern):首先,为可序列化的类设计一个私有的静态嵌套类,精确地表示外围类的实例的逻辑状态。这个嵌套类称为序列化代理,它有一个单独的构造器,其参数就是那个外围类,这个构造器只从它的参数中复制数据。外围类和其序列代理都必须声明实现Serializable接口。

        import java.io.InvalidObjectException;    import java.io.ObjectInputStream;    import java.io.Serializable;    import java.util.Date;    /**     * Created by laneruan on 2017/8/3.     *     */    //代理类SerializationProxy    public 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 = 234038490L;        private void readObject(ObjectInputStream stream)                throws InvalidObjectException{            throw new InvalidObjectException("Proxy required");        }        //它返回一个逻辑上的外围类实例,这是该模式的魅力所在        // 导致序列化系统在反序列化时将序列化代理类转变回外围类的实例        private Object readResolve(){            return new Period(start,end);        }    }    //外围类Period    class Period implements Serializable{        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());            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;        }        //有了这个方法,序列化系统永远不会产生外围类的序列化实例        private Object writeReplace(){            return new SerializationProxy(this);        }    }
  2. 这种代理模式的优点:不必太费心思,不必显式地执行有效性检查,不必知道哪些域可能会受到狡猾的序列化攻击的危险。

  3. 序列化代理模式的两个局限:不能与可以被客户端扩展的类兼容,也不能与对象图中包含循环的某些类兼容;开销增大。

  4. 总结:当你发现自己必须在一个不能被客户端扩展的类上编写readObject或者writeObject的时候,就应该考虑使用序列化代理模式;想要稳健地将带有重要约束条件的对象序列化时,这种模式可能是最容易的方法。