如果没有先认真考虑默认的序列化形式是否合适,则不要贸然接受
接受默认的序列化是一个非常重要的决定,你需要从灵活性,性能和正确性多个维度考虑。一般来讲,只有当你自行设计的自定义序列化形式与默认基本相同时,才能接受默认的序列化。
默认的序列化形式按顺序保存所有的非static 非 transient 字段。
实际上,有些字段属性应该是中间过程使用,或者并非实际有效的信息属性。比如 ,集合框架中的 modCount,它是用来检查是否发生了并发修改的。这个值并不代表集合中的信息,所以,其实是不需要保存的数据。
有些值确实是信息,但并非是最简化的有效信息。比如 ArrayList 中,每次扩容都要创建一个更大的数组。而在实际使用的时候,整个数组并不是全部有意义,它只在 size 对应的下标之前有意义。也就是,一个List 可能内部数组实际长度是50,而已使用可能是15。如果把整个数组全部序列化,是很浪费的。所以序列化时候要考虑这个问题,只序列化有效的部分。
再比如,对于 LinkedList ,内部用了 Node 封装了保存的元素这种情况下,默认的序列化就会把 Node 中的所有元素也全部序列化保存。要避免这种浪费,就应该自定义序列化,只保存元素信息和顺序。至于 Node 本身的信息,应该在反序列化的时候重新创建实例,并按顺序保存到LinkedList 中
如果一个对象的实际信息与需要的信息时一致的,可以考虑使用默认的序列化
即便确定了默认的序列化形式是合适的,通常还是建议提供一个readObject 方法来保证约束关系和安全性。
在上一节讨论过这个问题,有些属性是带有约束的。序列化是绕过构造器的创建实例方式。所以,需要在序列化的时候考虑约束关系。
过多消耗空间
大多数时候,内部的信息并不完全需要被保存,只要有部分相关的运行时信息,我们完全可以依赖这些数据还原。
比如对于一个List ,我们不需要序列化其内部的数组,或者 node 链,只需要知道内部的所有元素,就可以恢复这些数据。其内部很多 elementData size 等数据完全就是根据元素计算出来的
过多消耗时间
序列化的逻辑并不了解对象本身的逻辑,它只能沿着默认的途径去遍历。过多并不需要的数据也就意味着需要更多的遍历过程。
可能引起栈溢出
因为序列化和反序列化是一个方法调用。对于List 等,原来的添加是分批次添加的,也可能一个个添加。如果把它放在一个默认的序列化方法中,在一个方法中执行过多的遍历,可能引起栈溢出。
比如,自己构造一个简单的list 并使用默认的序列化形式,如果此list 中有几千上万的元素,就可能引起溢出问题。在实际开发中,这样的列表并不算很大。默认的序列化形式显然无法满足
注意:在反序列化的时候,需要考虑 transient 修饰的字段的恢复,使用transient修饰的字段在序列化的时候会被忽略,在反序列化的时候会成为默认值,但实际上这些值应该是根据当时实例的状态计算出来的。
比如LinkedList 中 size 字段,如果你不处理,因为它是int,就会被初始化为默认的 0,其实应该根据实际元素的数量,重新计算出 size。
无论是否使用默认的序列化,都需要注意同步问题
如果此对象的某些方法需要同步,则序列化时候也需要同步,比如 Vector 是一个同步的容器,它的序列化方法
private void writeObject(java.io.ObjectOutputStream s)
throws java.io.IOException {
final java.io.ObjectOutputStream.PutField fields = s.putFields();
final Object[] data;
synchronized (this) {
fields.put("capacityIncrement", capacityIncrement);
fields.put("elementCount", elementCount);
data = elementData.clone();
}
fields.put("elementData", data);
s.writeFields();
}
当然,readObject 不需要同步。因为在反序列化成功之前,对象还没有产生,不存在同步问题
强烈建议,无论是否默认的序列化方法,主动声明 serialVersionUID
避免因为 uid 问题导致的不兼容,当 uid不同时,反序列化会报异常失败
性能有所提升:如果没有声明 uid ,jvm 会在序列化时,自动计算出一个 uid
如果类发生了任何一点变化,可能uid 都会不同,导致反序列化时出错
所以,如果不声明 uid ,就会导致可能一个小小的无关的变动,都会导致序列化不兼容