课程咨询 :18087159764

  • 计算机程序的序列化

    发布:广州C++培训      来源:达内新闻      时间:2017-01-10

  • 广州C++培训的小编给大家分享序列化的神奇之处。

    定制序列化

    默认的序列化机制已经很强大了,它可以自动将对象中的所有字段自动保存和恢复,但这种默认行为有时候不是我们想要的。

    比如,对于有些字段,它的值可能与内存位置有关,比如默认的hashCode()方法的返回值,当恢复对象后,内存位置肯定变了,基于原内存位置的值也就没有了意义。还有一些字段,可能与当前时间有关,比如表示对象创建时的时间,保存和恢复这个字段就是不正确的。

    还有一些情况,如果类中的字段表示的是类的实现细节,而非逻辑信息,那默认序列化也是不适合的。为什么不适合呢?因为序列化格式表示一种契约,应该描述类的逻辑结构,而非与实现细节相绑定,绑定实现细节将使得难以修改,破坏封装。

    比如,我们在容器类中介绍的LinkedList,它的默认序列化就是不适合的,为什么呢?因为LinkedList表示一个List,它的逻辑信息是列表的长度,以及列表中的每个对象,但LinkedList类中的字段表示的是链表的实现细节,如头尾节点指针,对每个节点,还有前驱和后继节点指针等。

    那怎么办呢?Java提供了多种定制序列化的机制,主要的有两种,一种是transient关键字,另外一种是实现writeObject和readObject方法。

    将字段声明为transient,默认序列化机制将忽略该字段,不会进行保存和恢复。比如,类LinkedList中,它的字段都声明为了transient,如下所示:

    transient int size = 0;

    transient Node<E> first;

    transient Node<E> last;

    声明为了transient,不是说就不保存该字段了,而是告诉Java默认序列化机制,不要自动保存该字段了,可以实现writeObject/readObject方法来自己保存该字段。

    类可以实现writeObject方法,以自定义该类对象的序列化过程,其声明必须为:

    private void writeObject(java.io.ObjectOutputStream s) throws java.io.IOException

    可以在这个方法中,调用ObjectOutputStream的方法向流中写入对象的数据。比如,LinkedList使用如下代码序列化列表的逻辑数据:

    private void writeObject(java.io.ObjectOutputStream s)

    throws java.io.IOException {

    // Write out any hidden serialization magic

    s.defaultWriteObject();

    // Write out size

    s.writeInt(size);

    // Write out all elements in the proper order.

    for (Node<E> x = first; x != null; x = x.next)

    s.writeObject(x.item);

    }

    需要注意的是第一行代码:

    s.defaultWriteObject();

    这一行是必须的,它会调用默认的序列化机制,默认机制会保存所有没声明为transient的字段,即使类中的所有字段都是transient,也应该写这一行,因为Java的序列化机制不仅会保存纯粹的数据信息,还会保存一些元数据描述等隐藏信息,这些隐藏的信息是序列化之所以能够神奇的重要原因。

    与writeObject对应的是readObject方法,通过它自定义反序列化过程,其声明必须为:

    private void readObject(java.io.ObjectInputStream s) throws java.io.IOException, ClassNotFoundException

    在这个方法中,调用ObjectInputStream的方法从流中读入数据,然后初始化类中的成员变量。比如,LinkedList的反序列化代码为:

    private void readObject(java.io.ObjectInputStream s)

    throws java.io.IOException, ClassNotFoundException {

    // Read in any hidden serialization magic

    s.defaultReadObject();

    // Read in size

    int size = s.readInt();

    // Read in all elements in the proper order.

    for (int i = 0; i < size; i++)

    linkLast((E)s.readObject());

    }

    注意第一行代码:

    s.defaultReadObject();

    这一行代码也是必须的。

    序列化的基本原理

    稍微总结一下:

    如果类的字段表示的就是类的逻辑信息,如上面的Student类,那就可以使用默认序列化机制,只要声明实现Serializable接口即可。

    否则的话,如LinkedList,那就可以使用transient关键字,实现writeObject和readObject来自定义序列化过程。

    Java的序列化机制可以自动处理如引用同一个对象、循环引用等情况。

    但,序列化到底是如何发生的呢?关键在ObjectOutputStream的writeObject和ObjectInputStream的readObject方法内。它们的实现都非常复杂,正因为这些复杂的实现才使得序列化看上去很神奇,我们简单介绍下其基本逻辑。

    writeObject的基本逻辑是:

    如果对象没有实现Serializable,抛出异常NotSerializableException。

    每个对象都有一个编号,如果之前已经写过该对象了,则本次只会写该对象的引用,这可以解决对象引用和循环引用的问题。

    如果对象实现了writeObject方法,调用它的自定义方法。

    默认是利用反射机制(反射我们留待后续文章介绍),遍历对象结构图,对每个没有标记为transient的字段,根据其类型,分别进行处理,写出到流,流中的信息包括字段的类型即完整类名、字段名、字段值等。

    readObject的基本逻辑是:

    不调用任何构造方法。

    它自己就相当于是一个独立的构造方法,根据字节流初始化对象,利用的也是反射机制。

    在解析字节流时,对于引用到的类型信息,会动态加载,如果找不到类,会抛出ClassNotFoundException。

    版本问题

    上面的介绍,我们忽略了一个问题,那就是版本问题。我们知道,代码是在不断演化的,而序列化的对象可能是持久保存在文件上的,如果类的定义发生了变化,那持久化的对象还能反序列化吗?

    默认情况下,Java会给类定义一个版本号,这个版本号是根据类中一系列的信息自动生成的。在反序列化时,如果类的定义发生了变化,版本号就会变化,与流中的版本号就会不匹配,反序列化就会抛出异常,类型为java.io.InvalidClassException。

    通常情况下,我们希望自定义这个版本号,而非让Java自动生成,一方面是为了更好的控制,另一方面是为了性能,因为Java自动生成的性能比较低,怎么自定义呢?在类中定义如下变量:

    private static final long serialVersionUID = 1L;

    在Java IDE如Eclipse中,如果声明实现了Serializable而没有定义该变量,IDE会提示自动生成。这个变量的值可以是任意的,代表该类的版本号。在序列化时,会将该值写入流,在反序列化时,会将流中的值与类定义中的值进行比较,如果不匹配,会抛出InvalidClassException。

    那如果版本号一样,但实际的字段不匹配呢?Java会分情况自动进行处理,以尽量保持兼容性,大概分为三种情况:

    字段删掉了:即流中有该字段,而类定义中没有,该字段会被忽略。

    新增了字段:即类定义中有,而流中没有,该字段会被设为默认值。

    字段类型变了:对于同名的字段,类型变了,会抛出InvalidClassException。

    高级自定义

    除了自定义writeObject/readObject方法,Java中还有如下自定义序列化过程的机制:

    Externalizable接口

    readResolve方法

    writeReplace方法

    这些机制实际用到的比较少,我们简要说明下。

    Externalizable是Serializable的子接口,定义了如下方法:

    void writeExternal(ObjectOutput out) throws IOException

    void readExternal(ObjectInput in) throws IOException, ClassNotFoundException

    与writeObject/readObject的区别是,如果对象实现了Externalizable接口,则序列化过程会由这两个方法控制,默认序列化机制中的反射等将不再起作用,不再有类似defaultWriteObject和defaultReadObject调用,另一个区别是,反序列化时,会先调用类的无参构造方法创建对象,然后才调用readExternal。默认的序列化机制由于需要分析对象结构,往往比较慢,通过实现Externalizable接口,可以提高性能。

    readResolve方法返回一个对象,声明为:

    Object readResolve() 

    如果定义了该方法,在反序列化之后,会额外调用该方法,该方法的返回值才会被当做真正的反序列化的结果。这个方法通常用于反序列化单例对象的场景。

    writeReplace也是返回一个对象,声明为:

    Object writeReplace()

    如果定义了该方法,在序列化时,会先调用该方法,该方法的返回值才会被当做真正的对象进行序列化。

    writeReplace和readResolve可以构成一种所谓的序列化代理模式,这个模式描述在<Effective Java>第二版78条中,Java容器类中的EnumSet使用了该模式,我们一般用的比较少,就不详细介绍了。

    序列化特点分析

    序列化的主要用途有两个,一个是对象持久化,另一个是跨网络的数据交换、远程过程调用。

    Java标准的序列化机制有很多优点,使用简单,可自动处理对象引用和循环引用,也可以方便的进行定制,处理版本问题等,但它也有一些重要的局限性:

    Java序列化格式是一种私有格式,是一种Java语言特有的技术,不能被其他语言识别,不能实现跨语言的数据交换。

    Java在序列化字节中保存了很多描述信息,使得序列化格式比较大。

    Java的默认序列化使用反射分析遍历对象结构,性能比较低。

    Java的序列化格式是二进制的,不方便查看和修改。

    由于这些局限性,实践中往往会使用一些替代方案。在跨语言的数据交换格式中,XML/JSON是被广泛采用的文本格式,各种语言都有对它们的支持,文件格式清晰易读,有很多查看和编辑工具,它们的不足之处是性能和序列化大小,在性能和大小敏感的领域,往往会采用更为精简高效的二进制方式如ProtoBuf, Thrift, MessagePack等。

    小结

    本节广州C++培训的小编介绍了Java的标准序列化机制,我们介绍了它是一种神奇的机制,通过简单的Serializable接口就能自动处理很多复杂的事情,但它也有一些重要的限制,最重要的是不能跨语言。

上一篇:序列化的复杂对象

下一篇:【广州C++培训机构】RSA算法

最新开班日期  |  更多

c++--零基础周末班

c++--零基础周末班

开班日期:7月31日

c++--零基础全日制班

c++--零基础全日制班

开班日期:7月31日

c++--免费训练营

c++--免费训练营

开班日期:7月31日

c++--高薪就业班

c++--高薪就业班

开班日期:7月31日

  • 网址:http://gz.c.tedu.cn     地址:广州市天河北五山路 141 号尚德大厦 627
  • 课程培训电话:18087159764     全国服务监督电话:400-111-8989
  • 服务邮箱 tousu@tedu.cn
  • 2001-2016 达内时代科技集团有限公司 版权所有 京ICP证8000853号-56