序列化再探讨

Posted borter

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了序列化再探讨相关的知识,希望对你有一定的参考价值。

  从以上技术的讨论中我们不难体会到,序列化是Java之所以能够出色地实现其鼓吹的两大卖点??分布式(distributed)和跨平台(OS independent)的一个重要基础。TIJ(即“Thinking in Java”)谈到I/O系统时,把序列化称为“lightweight persistence”??“轻量级的持久化”,这确实很有意思。

 

★为什么叫做“序列”化?

    开场白里我说更习惯于把“Serialization”称为“序列化”而不是“串行化”,这是有原因的。介绍这个原因之前先回顾一些计算机基本的知识,我们知道现代计算机的内存空间都是线性编址的(什么是“线性”知道吧,就是一个元素只有一个唯一的“前驱”和唯一的“后继”,当然头尾元素是个例外;对于地址来说,它的下一个地址当然不可能有两个,否则就乱套了),“地址”这个概念推广到数据结构,就相当于“指针”,这个在本科低年级大概就知道了。注意了,既然是线性的,那“地址”就可以看作是内存空间的“序号”,说明它的组织是有顺序的,“序号”或者说“序列号”正是“Serialization”机制的一种体现。为什么这么说呢?譬如我们有两个对象a和b,分别是类A和B的实例,它们都是可序列化的,而A和B都有一个类型为C的属性,根据前面我们说过的原则,C当然也必须是可序列化的。

1. import java.io.*;

2. ...

3. class A implements Serializable

4. {

5.   C c;

6.   ...

7. }

8. 

9. class B implements Serializable

10. {

11.   C c;

12.   ...

13. }

14.

15. class C implements Serializable

16. {

17.   ...

18. }

19.

20. A a;

21. B b;

22. C c1;

23. ...

 

    注意,这里我们在实例化a和b的时候,有意让他们的c属性使用同一个C类型对象的引用,譬如c1,那么请试想一下,但我们序列化a和b的时候,它们的c属性在外部字节流(当然可以不仅仅是文件)里保存的是一份拷贝还是两份拷贝呢?序列化在这里使用的是一种类似于“指针”的方案:它为每个被序列化的对象标上一个“序列号”(serial number),但序列化一个对象的时候,如果其某个属性对象是已经被序列化的,那么这里只向输出流写入该属性的序列号;从字节流恢复被序列化的对象时,也根据序列号找到对应的流来恢复。这就是“序列化”名称的由来!这里我们看到“序列化”和“指针”是极相似的,只不过“指针”是内存空间的地址链,而序列化用的是外部流中的“序列号链”。

    使用“序列号”而不是内存地址来标识一个被序列化的对象,是因为从流中恢复对象到内存,其地址可能就未必是原来的地址了??我们需要的只是这些对象之间的引用关系,而不是死板的原始位置,这在RMI中就更是必要,在两台不同的机器之间传递对象(流),根本就不可能指望它们在两台机器上都具有相同的内存地址。 

 

★更灵活的“序列化”:transient属性和Externalizable

    Serializable确实很方便,方便到你几乎不需要做任何额外的工作就可以轻松将内存中的对象保存到外部。但有两个问题使得Serializable的威力收到束缚:

    一个是效率问题,《Core Java 2》中指出,Serializable使用系统默认的序列化机制会影响软件的运行速度,因为需要为每个属性的引用编号和查号,再加上I/O操作的时间(I/O和内存读写差的可是一个数量级的大小),其代价当然是可观的。

    另一个困扰是“裸”的Serializable不可定制,傻乎乎地什么都给你序列化了,不管你是不是想这么做。其实你可以有至少三种定制序列化的选择。其中一种前面已经提到了,就是在implements Serializable的类里面添加私有的writeObject()和readObject()方法(这种Serializable就不裸了,技术分享图片),在这两个方法里,该序列化什么,不该序列化什么,那就由你说了算了,你当然可以在这两个方法体里面分别调用ObjectOutputStream.defaultWriteObject()和ObjectInputStream.defaultReadObject()仍然执行默认的序列化动作(那你在代码上不就做无用功了?呵呵),也可以用ObjectOutputStream.writeObject()和ObjectInputStream.readObject()方法对你中意的属性进行序列化。但虚拟机一看到你定义了这两个方法,它就不再用默认的机制了。

    如果仅仅为了跳过某些属性不让它序列化,上面的动作似乎显得麻烦,更简单的方法是对不想序列化的属性加上transient关键字,说明它是个“暂态变量”,默认序列化的时候就不会把这些属性也塞到外部流里了。当然,你如果定义writeObject()和readObject()方法的化,仍然可以把暂态变量进行序列化。题外话,像transient、violate、finally这样的关键字初学者可能会不太重视,而现在有的公司招聘就偏偏喜欢问这样的问题 :(

    再一个方案就是不实现Serializable而改成实现Externalizable接口。我们研究一下这两个接口的源代码,发现它们很类似,甚至容易混淆。我们要记住的是:Externalizable默认并不保存任何对象相关信息!任何保存和恢复对象的动作都是你自己定义的。Externalizable包含两个public的方法:

1. public void writeExternal(ObjectOutput out) throws IOException;

2. public void readExternal(ObjectInput in) throws IOException, ClassNotFoundException;

 

    乍一看这和上面的writeObject()和readObject()几乎差不多,但Serializable和Externalizable走的是两个不同的流程:Serializable在对象不存在的情况下,就可以仅凭外部的字节序列把整个对象重建出来;但Externalizable在重建对象时,先是调用该类的默认构造函数(即不含参数的那个构造函数)使得内存中先有这么一个实例,然后再调用readExternal方法对实例中的属性进行恢复,因此,如果默认构造函数中和readExternal方法中都没有赋值的那些属性,特别他们是非基本类型的话,将会是空(null)。在这里需要注意的是,transient只能用在对Serializable而不是Externalizable的实现里面。 

 

★序列化与克隆

    从“可序列化”的递归定义来看,一个序列化的对象貌似对象内存映象的外部克隆,如果没有共享引用的属性的化,那么应该是一个深度克隆。关于克隆的话题有可以谈很多,这里就不细说了,有兴趣的话可以参考IBM developerWorks上的一篇文章:JAVA中的指针,引用及对象的clone

 

以上是关于序列化再探讨的主要内容,如果未能解决你的问题,请参考以下文章

Notes 20180309 : String第一讲_char的可读序列

什么是序列化, pickle, shelve(春节再整理), json, configparser(春节再整理)模块

Java序列化与反序列化

对象的序列化

Java序列化与反序列化

Java序列化与反序列化