Java序列化机制提供了一个框架,用来将对象编码成字节流,并从字节流编码中重新构建对象。一旦对象被序列化之后,就可以被存储在磁盘上,或者网络另外一台计算机上。序列化技术为远程通信提供了标准的线路级对象表示法,也为JavaBeans组件提供了标准的持久化数据格式。
本章内容导图:
1.谨慎地实现Serializable接口
使一个类的实例可被序列化,只需要让它实现Serializable接口即可。因为简单,程序员普遍对序列化存在误解,以为不需要做什么工作就可以实现序列化,实际的情形却复杂的多。
实现Serializable的代价有:
1.一旦一个类被发布,就大大降低了改变这个类的实现的灵活性
一个类实现了Serializable接口,它的字节流编码就变成了导出API的一部分。一旦这个类被广泛使用,就必须永远支持这种序列化形式。如果不认真设计一种自定义的序列化形式,而仅仅接受默认的序列化形式,这种序列化形式将被永远地束缚在该类最初的内部表示法上。
2.它增加了出现bug和安全漏洞的可能性
通常是用构造器来构建对象的,序列化机制提供一种语言之外的对象创建机制,反序列化机制是一个隐藏的构造器,具备与其他构造器相同的特点。默认的反序列化机制很容易使对象的约束关系遭到破坏,也易遭受非法访问。
3.随着类发行新的版本,相关的测试负担也增加了
一旦发布,需要确保各版本的兼容性
每当实现一个类的时候,都需要权衡一下实现序列化所付出的代价和带来的好处。
内部类不应该实现Serializable。
静态成员类可以实现Serializable。
实现Serializable接口代表一个承诺,必须仔细考虑。
如果一个类是为了继承而设计的,需要倍加留意子类序列化的需求。对于为继承而设计的不可序列化的类,应该考虑提供一个无参构造器,否则子类就无法进行序列化。
2.考虑使用自定义的序列化形式
如果没有认真考虑默认的序列化形式是否合适,就不要贸然接受。接受默认的序列化形式是一个非常重要的决定,需要从灵活性、性能和正确性多个角度对这种编码形式进行考察。一般来讲,只有当你自行设计的自定义序列化形式与默认的序列化形式基本相同时,才能接受默认的序列化形式。
即便是确定了默认的序列化形式是合适的,通常还必须提供一个readObject方法以保证约束关系和安全性。
当一个对象的物理表示法与它的逻辑数据内容有实质性的区别时,使用默认序列化形式有以下4个缺点:
1.它使这个类的导出API永远地束缚在该类的内部表示法上。
序列化字节流也是导出API的一部分,也代表一种约定。
2.它会消耗过多的空间。
例如链表的序列化,不仅表示链表中的每个项,还要表示所有的连接关系,这是不必要的。
3.它会消耗过多的时间。
序列化逻辑并不了解对象图的拓扑结构,所以它必须要经过一个昂贵的图遍历过程。
4.它会引起栈溢出。
默认的序列化过程要对对象图执行一次递归遍历,即使对于中等规模的对象图,这样的操作也可能会引起栈溢出。
无论是否使用默认的序列化形式,当defaultWriteObject方法被调用的时候,每一个未被标记为transient的实例域都会被序列化。
不管选择哪种序列化形式,都要为自己编写的每个可序列化的类声明一个显式的序列版本UID。
当决定要将一个类做成可序列化的时候,应该仔细考虑采用什么样的序列化形式。只有当默认的序列化形式能够合理地描述对象的逻辑状态时,才能使用默认的序列化形式;否则就要设计一个自定义的序列化形式,通过它合理地描述对象的状态。
应该分配足够多的时间来设计类的序列化形式,就像要分配足够多的时间来设计它的导出API一样。你无法在将来的版本中去掉导出方法,也不能去掉序列化形式中的域,它们必须被永久地保留下去,以确保序列化兼容性。选择错误的序列化形式对于一个类的复杂性和性能都会有永久的负面影响。
3.保护性地编写readObject方法
readObject相当于另一个公有的构造器,如同其他的构造器一样,它也要求注意同样的所有注意事项:必须检查参数的有效性,必要的时候对参数进行保护性拷贝。如果readObject无法做到这两点,就易遭受攻击。
编写健壮readObject方法的指导方针:
1.对于对象引用域必须保持为私有的类,要保护性地拷贝这些域中的每个对象。不可变类的可变组件就属于这一类别。
2.对于任何约束条件,如果检查失败,则抛出一个InvalidObjectException异常。这些检查应该跟在所有的保护性拷贝之后。
3.如果整个对象图在被反序列化之后必须进行验证,就应该使用ObjectInputValidation接口。
4.无论是直接方式还是间接方式,都不要调用类中任何可覆盖的方法。
每当编写readObject方法的时候,要想到正在编写一个公有的构造器,无论给它传递什么样的字节流,它都必须产生一个有效的实例。
4.对于实例控制,枚举类型优先于readResolve
不管是采用默认的序列化方式,还是自定义的序列化方式,都相当于对外部提供了一个公有的构造器。对于可序列化的实例受控类(如可序列化的单例类),readResolve方法提供了一种控制实例构建的方法。
readResolve方法允许你用readObject创建的实例代替另一个实例。对于一个正在被反序列化的对象,如果它的类定义了一个readResolve方法,并且具备正确的声明,那么在反序列化之后,新建对象上的readResolve方法就会被调用,该方法返回的对象引用将被返回,取代新建的对象。在这个特性的绝大多数用法中,指向新建对象的引用不需要再被引用,立即成为垃圾回收的对象。
如果依赖readResolve进行实例控制,带有对象引用的所有实例域都必须声明为transient的,否则就可能会被攻击。
将一个可序列化的实例受控类编写成枚举,就可以绝对保证除了所声明的常量之外,不会有别的实例,这是由JVM保障的。
public enum Elvis {
INSTANCE;
private String[] favoriteSongs = {"Hound Dog", "Heartbreak Hotel"};
public void printFavorites() {
System.out.println(Arrays.toString(favoriteSongs));
}
}
应该尽可能地使用枚举类型来实施实例控制的约束条件。如果做不到,同时又需要一个既可序列化又是实例受控的类,就必须提供一个readResolve方法,并确保该类的所有实例域都为基本类型,或者是transient的。
5.考虑用序列化代理代替序列化实例
实现Serializable接口,会增加出错和出现安全问题的可能性,因为它是利用语言之外的机制来创建对象的,而不是使用普通的构造器。序列化代理模式可以极大地减少上述风险的发生。
实现序列化代理模式的方法:
1.为可序列化的类设计一个私有的静态嵌套类,精确地表示外围类实例的逻辑状态。
2.将writeReplace方法添加到外围类中:
private Object writeReplace() {
return new SerializationProxy(this);
}
3.在外围类中添加如下readObject方法:
private void readObject(ObjectInputStream in) throws InvalidObjectException {
throw new InvalidObjectException("Proxy required");
}
4.在代理类中提供一个readResolve方法,它返回一个逻辑上相当的外围类实例:
private Object readResolve() {
return new Period(start, end);
}
序列化代理模式的完整实现为:
public final 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());
}
private void readObject(ObjectInputStream in) throws InvalidObjectException {
throw new InvalidObjectException("Proxy required");
}
//将外围类的实例转变成了它的序列化代理
private Object writeReplace() {
return new SerializationProxy(this);
}
private static class SerializationProxy implements Serializable {
private final Date start;
private final Date end;
SerializationProxy(Period p) {
this.start = p.start;
this.end = p.end;
}
private Object readResolve() {
return new Period(start, end);
}
private static final long serialVersionUID = 234098243823485285L;
}
}
如果必须在一个不能被客户端扩展的类上编写readObject或者writeObject方法的时候,就应该考虑使用序列化代理模式。