74. 谨慎的实现serializable接口
实现serializable的代价
- 最大的代价:一旦一个类被发布,就大大降低了“改变这个类的实现”的灵活性
- 增加了出现bug和安全漏洞的可能性,反序列化是一个隐藏的构造器,依靠默认的反序列化机制,很容易是对象的约束关系遭到破坏,以及遭到非法访问
随着类发行新的版本,相关的测试负担也增加了。一个可序列化的类被修改时,要检查是否可以“在新版本中序列化一个实例,在旧版本中反序列化该实例”,反之亦然。
- 为了继承而设计的类应该尽可能少的去实现Serializable接口,用户自定义的接口也应该尽可能少的继承Serializable接口。例外,Throwable、Component和HttpServlet抽象类
- 内部类不应该实现Serializable即可
- 静态成员类可以实现Serializable接口
75. 考虑使用自定义的序列化形式
对于一个对象来说,理想的序列化形式应该只包含该对象所表示的逻辑数据,而逻辑数据与物理表示法(存储结构)应该是独立的。如果一个对象的物理表示法等同于它的逻辑内容,就适用于使用默认的序列化形式。如:
public class Name implements Serializable { /** * Last name. Must be non-null. * @serial */ private final String lastName; /** * first name. Must be non-null. * @serial */ private final String firstName; private final String middleName; .... }
在这段代码中,Name类的实例域精确的反应了它的逻辑内容,可以使用默认的序列化形式。注意:虽然lastName、firstName和middleName域是私有的,但它们仍然需要有注释文档。因为,这些私有域定义了一个公有的API,即这个类的序列化形式。@serial标签用来告知Javadoc工具,把这些文档信息放在有关序列化形式的特殊文档页中。
当一个对象的物理表示法与它的逻辑内容之间有实质性的不同时,使用默认序列化形式有如下缺点:
- 它将这个类的导出API永远束缚在了该类的内部表示法上。如,私有内部类变成公有API的一部分。
- 会消耗过多的空间和时间
- 会引起栈溢出
其约束关系可能遭到严重破坏,如散列表
//默认序列化形式 public final class StringList implements Serializable { private int size = 0; private Entry head = null; private static class Entry implements Serializable { String data; Entry next; Entry previous; } .... }
- 自定义序列化:
public final class StringList implements Serializable { private static final long serialVersionUID = ...; private transient int size = 0; //不会被序列化 private transient Entry head = null; private static class Entry { String data; Entry next; Entry previous; } public final void add(String s) { ... } /** * Serialize this {@code StringList} instance * * @serialData The size of the list (the number of strings it contains) * is emitted ({@code int}), followed by all of its elements (each a * {@code String}), in the proper sequence. */ private void writeObject(ObjectOutputStream s) throws IOException { s.defaultWriteObject(); s.writeInt(size); for(Entry e = head; e != null; e = e.next ) { s.writeObject(e.data); } } private void readObject(ObjectInputStream s) throws IOException, ClassNotFoundException { s.defaultReadObject(); int num = s.readInt(); for(int i=0; i < num; i++) { add((String)s.readObject()); } } ..... }
注意:尽管StringList的所有域都是transient,但writeObject和readObject的首要任务仍是调用defaultXxxObject方法,这样可以极大的增强灵活性。另外尽管writeObject是私有的,仍然需要文档注释。
无论自定义序列化还是默认序列化,对于一个线程安全的对象,必须在序列化方法上强制同步。如:
private synchronized void writeObject(ObjectOutputStream s) throws IOException { s.defaultWriteObject(); }
总之,当要将一个类序列化时,应该仔细考虑采用默认序列化还是自定义序列化。选择错误的序列化形式对于一个类的复杂性和性能都会有永久的负面影响
76. 保护性编写readObject方法
编写readObject方法的指导原则
- 对于对象引用域必须保持为私有的类,要保护性的拷贝这些域中的每个对象
- 对于任何约束条件,若检查失败,则抛出一个InvalidObjectException异常。检查应在保护性拷贝之后
- 无论直接方式还是间接方式,都不要调用类中任何可被覆盖的方法,否则反序列时可能会失败
77. 对于实例控制,枚举类型优先于readResolve
- 应该尽可能的使用枚举类型来实施实例控制的约束条件,若做不到,就必须提供一个readResolve方法,并将引用类型的域声明为transient的
78. 考虑用序列化代理代替序列化实例
序列化代理模式能够极大的减少实现Serializable接口所带来的风险。
实现序列化代理模式的步骤:
- 首先为可序列化的类设计一个私有的静态嵌套类,精确的表示外围类实例的逻辑状态。它有一个单独的构造器,其参数类型为外围类。外围类及其序列化代理都必须实现Serializable接口。
- 将writeReplace方法添加到外围类中。
在SerializableProxy类中提供readResolve方法,它返回逻辑上相等的外围类的实例。
//外围类不需要serialVersionUID 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 getStart() { return new Date(start.getTime()); } public Date getEnd() { return new Date(end.getTime()); } //在序列化之前,将外围类的实例转变成它的序列化代理 private Object writeReplace(){ return new SerializationProxy(this); } //防止被攻击者使用 private void readObject(ObjectInputStream stream) throws InvalidObjectException{ throw new InvalidObjectException("Proxy required"); } private static class SerializationProxy implements Serializable { private static final long serialVersionUID = ...; 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); } } }
正如保护性拷贝一样,序列化代理可以阻止伪字节流的攻击及内部域的盗用攻击。与使用保护性拷贝不同,使用序列化代理允许Period的域为final的,这可以保证Period类真正不可变。序列化代理模式更容易实现,它不必考虑哪些域会被序列化攻击,也不必显示的执行有效性检查。
序列化代理的局限性:不能与可以被客户端扩展的类兼容,也不能与对象图中包含循环的类兼容,比保护性拷贝性能低