最近在工作中指导新人开发,任务内容涉及到序列化,发现很多初学者对于序列化的概念以及使用的场景比较模糊,所以为他们总结了有关的Java Serialization的一些心得,这里记录出来与大家分享。由于关于序列化的内容很多,篇幅有限,不能全面的描述,本文只是侧重于序列化的概念以及如何设计序列化的类。如果是对序列化完全没有概念的朋友,也可以移步这里, 这是一篇优秀的介绍Java序列化的文章,相信会是开卷有益。欢迎大家留言指正。
关于自定义序列化的内容,请见Java序列化心得(二):自定义序列化
何为序列化:类对象的持久化
Java序列化接口(Serializable interface)提供框架将类的对象字节化,以二进制编码形式保存该对象的状态,并可以根据这样的字节编码复原出类的对象。
序列化的过程实际上是将一个对象“持久化”的过程:将内存中临时的Java对象固化为物理上可以存储和记录的字节形式。
有的人可能会有疑问为什么要序列化?很多程序的确没有使用序列化也可以完美运行,但是有的场景中,序列化将大有作为。
想一想网络通信中,常常需要把类对象的信息从一个终端传递到网络中的另一端,对象的“状态内容”在网络中就需要以字节的形式传送,可是这些字节到了另一端改如何恢复成类对象呢?再或者是任务队列的情况下,很多任务对象要放在队列中等待被执行,但是资源有限,如果队列中内容很多,都在内存中保存有时候不现实,需要把这些任务信息保存到硬盘上,等到能够执行的时候再从硬盘中重新读取到内存而后运行。诸如以上这种需要暂时把对象的信息保存起来的情况在实际的开发中会遇到很多,这种场景中就需要序列化的帮助。
换个角度来说,JVM(Java虚拟机)在内存中创建可复用的Java对象,并维护这些对象的生命周期,即这些对象的生命周期不会比JVM的生命周期更长,只有在JVM的控制范围内,这些对象才是有效地。但在现实应用中,就可能要求指定的对象暂时离开在JVM的控制范围(如果发送到网络中或者是保存到硬盘上),并在合适的时机重新回到JVM控制范围中。Java序列化API便是为处理对象以字节形式导出/移入JVM而设计的标准接口。
序列化的代价:哪些关于序列化的头疼事:
一个类实现序列化结构很简单,只要在定义类时声明* implements Serializable*,
public class Person implements Serializable
便可调用默认的序列化方法,生成默认的序列化格式(Default Serialization Form) ,这里需要强调两点:
- 序列化是保存对象的状态,既然是对象的序列化,自然只关心对象的域,而类的静态域不在序列化范围内;
- 在序列化对象时,不仅会序列化当前对象本身,还会对该对象引用的其它对象也进行序列化,所以要求所有在该对象序列化范围内的域都要求是可序列化的对象,这就将会出发一个迭代的过程。
虽然实现Serializable接口很简单,但决定是否要让一个类序列化却是比较头疼的事,往往不能只考虑序列化对象带来的便利,还要考虑到序列化所带来的代价,这种代价往往是因为序列化和类的继承结合在一起造成,《Effective Java》中列举了序列化的代价如下:
- 序列化将降低类结构的灵活性: 一旦被声明序列化,类的字节编码模式就像会成为输出API一部分,如果是默认的形式,序列化格式会反映出该类的原始内部结构,这样以来类的私有域也会成为输出API的一部分,这就破坏了信息隐藏的原则,因此要被序列化的类,其结构的设计往往要慎重;
- 序列化的过程可能带来Bug隐患和安全漏洞:如果代码中原来是接受一个类默认的序列化格式,但后来这个类的设计有变化,或者是这个类在被继承后子类添加了新的域,则默认序列化格式也会一起变化,此时将变化后的序列化格式传入原有代码中,就会由于序列化格式版本不一致,而造成不易被发觉的异常;
- 序列化将会增加新版本测试的难度:还是当序列化的类发生修改或是被继承时,序列化的版本可能有很多,为了测试代码的功能,就可能要产生多个版本的测试代码,而客户端为了接受多种序列化格式,也不得不增加冗余性。
在这里可以看出来,序列化是为了“固化”对象的信息,而继承是希望让类的对象可以多样化和动态化,二者之间在设计目标上有一定的矛盾。关于序列化和继承,有如下设计上的建议供读者参考:
基类不序列化,如果一个类是为了被广泛继承而设计的,尽量不要将其序列化,这样后患无穷,设计被序列化的类应该是结构比较固定的类;
当前的类可能不需要序列化,但是它的派生类在以后使用中不排除被序列化的可能,为了便于派生累的序列化,基类应该保留无参数的构造器,这是因为序列化的过程中,首先是要调用无参数构造器来生成类对象,其次才是往对象中填充各个域的值,如果累没有默认构造器就可能导致序列化无法进行。
定义类的序列化版本号, 来是验证接收的序列化格式和期待的序列化格式版本是否一致,其定义如下
private final static long serialVersionUID = 1L
默认情况下是根据类名、接口名、成员方法及属性等来生成一个64位的哈希字段,如果开发人员不希望通过编译来强制划分软件版本,即实现序列化接口的实体能够兼容先前版本,未作更改的类,就需要显式地定义一个名为serialVersionUID,类型为long的静态常量,只有版本号一致的类,其序列化的格式才能被相互接受,否者将会报错。有关序列化版本号的内容可以参考这里,关于版本号各种使用情况,都有详细的阐述。
默认序列化方法的问题:
因为用默认序列化格式的存在,类序列化的实现变得很轻松,但是正如上面提到的,默认的序列化格式有各种各样的短板,这些短板在特定的情况下会带来不小的麻烦,主要集中在一下几点:
- 默认的序列化格式将会体现类内部的格式,可能会暴露私有域;
- 默认序列化的过程是迭代的,这样可能会造成栈溢出;
- 默认序列化的过程可能消耗大量内存空间和时间;
第一点和第二点,上面的内容已经提过,这里不再多加赘述;关于第三点,默认序列化转化效率的对比,读者可以参考这篇文章,作者将GSON与之对比,结果差别十分明显。GSON不仅要向流中写入或读出数据,还需要对字符串进行词法分析,即使这样仍然比默认的序列化方式快3倍,这是因为默认的序列化过程中需要用到如反射之类的方式去取值,而且还需要处理与对象自身的一些信息。
正因为默认的序列化方式不能满足所有场景和需求,所以就需要引入定制的序列化方式(Custom Serialization Form),这部分内容将在Java序列化心得(二):自定义序列化中为大家介绍。