转载

《Effective Java》学习笔记十——序列化

本文会提到readObject、writeObject、readResolve方法,都和序列化相关,这里集中解释一下。此外,这一章还提到了readObjectNoData方法,也放到这里一起说明。

readObject、writeObject方法:这两个方法用于自定义序列化、反序列化的方式,如果一个类中有些成员的序列化形式希望自定义,需要重写这两个方法。注意这两个方法都是private的。在重写的这两个方法的内部,需要首先调用ObejectInputStream/ObjectOutputStream的defaultReadObject/defaultWriteObject方法对还需要默认序列化/反序列化的成员操作。 readResolve方法:因为反序列化操作也可以看作一个构造器,在程序中单例模式只需要一个类的实例,如果通过反序列化又得到了一个实例就违反了初衷,这时可以通过编写readResolve方法,返回当前 系统 中存在的这个单例的实例,而不是反序列化,来保持单例的正常工作。 readObjectNoData方法:在一些情况下(比如旧的类型反序列化),我们需要反序列化的类一开始就有一些约束条件,但是序列化出来的外部文件中并没有建立这个约束,这个时候可以编写这个方法,将需要建立约束的成员在这里赋值,避免这些成员初始化成默认值破坏类的状态。注意,这个方法也是private的。

谨慎地实现Serializable接口

序列化的含义和作用

序列化用来将对象编码成字节流,反序列化就使将字节流编码重新构建对象。

序列化实现了对象传输和对象持久化,所以它能够为远程通信提供对象表示法,为JavaBean组件提供持久化数据。

序列化的危害

  1. 降低灵活性:为实现Serializable而付出的最大代价是,一旦一个类被发布,就大大降低了”改变这个类的实现”的灵活性。如果一个类实现了Serializable,它的字节流编码(或者说序列化形式,serialized form)就变成了它的导出的API的一部分,必须永远支持这种序列化形式。

    而且,特殊地,每个可序列化类都有唯一的标志(serial version id,在类体现为私有静态final的long域serialVersionUID),如果没有显式指示,那么系统就会自动生成一个serialVersionUID,如果下一个版本改变了这个类,那么系统就会重新自动生成一个serialVersionUID。因此如果没有声明显式的uid,会破坏版本之间的兼容性,运行时产生InvalidClassException。

  2. 降低封装性:如果你接受了默认的序列化形式,这个类中私有的和包级私有的实例域将都变成导出的API的一部分,这不符合”最低限度地访问域”的实践准则。

  3. 降低安全性:增加了bug和漏洞的可能性,反序列化的过程其实类似于调用对象的构造器,但是这个过程又没有用到构造器,因此如果字节流被无意修改或被用心不测的人修改,那么服务器很可能会产生错误或者遭到攻击。16年出现的大名鼎鼎的Java反序列化漏洞本质上就是不恰当的序列化造成的。

  4. 降低可测试性:随着类版本的不断更替,必须满足版本兼容问题,所以发行的版本越多,测试的难度就越大。

  5. 降低性能:序列化对象时,不仅会序列化当前对象本身,还会对该对象引用的其他对象也进行序列化。如果一个对象包含的成员变量是容器类等并深层引用时(对象是链表形式),此时序列化开销会很大,这时必须要采用其他一些手段处理。

序列化的使用场景

  1. 需要实现一个类的对象传输或者持久化。
  2. A是B的组件,当B需要序列化时,A也实现序列化会更容易让B使用。

序列化不适合场景

为了继承而设计的类应该尽可能少地去实现Serializable接口,用户接口也应该尽可能不继承Serializable接口,原因是子类或实现类也要承担序列化的风险。

序列化需要注意的地方

  1. 如果父类实现了Serializable,子类自动序列化了,不需要实现Serializable;
  2. 若父类未实现Serializable,而子类序列化了,父类属性值不会被保存,反序列化后父类属性值丢失,需要父类有一个无参的构造器,子类要负责序列化(反序列化)父类的域,子类要先序列化自身,再序列化父类的域。

至于为什么需要父类有一个无参的构造器,是因为子类先序列化自身的时候先调用父类的无参的构造器。

实例:

private void writeObject(java.io.ObjectOutputStream out) throws IOException { 
   out.defaultWriteObject();//先序列化对象 
   out.writeInt(parentvalue);//再序列化父类的域 
} 
private void readObject(java.io.ObjectInputStream in) throws IOException, ClassNotFoundException{ 
   in.defaultReadObject();//先反序列化对象 
     parentvalue=in.readInt();//再反序列化父类的域 
}
  1. 序列化时,只对对象状态进行了保存,对象方法和类变量等并没有保存,因此序列化并不保存静态变量值。

  2. 当一个对象的实例变量引用其他对象,序列化该对象时也把引用对象序列化了。所以组件也应该序列化。

  3. 不是所有对象都可以序列化,基于安全和资源方面考虑,如Socket/thread若可序列化,进行传输或保存,无法对他们重新分配资源。

考虑使用自定义的序列化方式

如果没有先认真考虑默认的序列化形式是否合适,则不要贸然接受。

一个对象为根的对象图,相对于它的物理表示法而言,该对象的默认序列化形式是一种比较有效的编码形式。换句话说,默认的序列化形式描述了该对象内部所包含的数据,以及每一个可以从这个对象到达的其他对象的内部数据。它也描述了所有这些对象被链接起来后的拓扑结构。对于一个对象来说,理想的序列化形式应该只包含该对象所表示的逻辑数据,而逻辑数据与物理表示法应该是各自独立的。

如果一个对象的物理表示法等同于它的逻辑内容,可能就适合于使用默认的序列化形式。

即使你确定了默认的序列化形式是合适的,通常还必须提供一个readObject方法以保证约束关系和安全性。

当一个对象的物理表示法与它的逻辑数据内容有实质性的区别时,使用默认序列化形式会有以下四个缺点:

  • 它使这个类的导出API永远地束缚在该类的内部表示法上。
  • 它会消耗过多的空间。
  • 它会消耗过多的时间。
  • 它会引起栈溢出。

无论你是否使用默认的序列化形式,如果在读取整个对象状态的任何其他方法上强制任何同步,则也必须在对象序列化上强制这种同步。

不管你选择了哪种序列化形式,都要为自己编写的每个可序列化的类生命一个显式的序列版本UID(serial Version UID)。

保护性地编写readObject方法

对于序列化形成的字节流,并不都是安全的,里面可能有伪造的有害数据,对它们不加分辨地反序列化,可能会导致程序收到损害。伪造的有害数据一方面可以使不正确的字节流 ;另一方面还可能是在正确的字节流中夹带的“私货”,通过“私货”可以恶意修改反序列化的对象。

本条建议:

在readObject反序列化之后,检查对象成员的有效性。 进行保护性拷贝(关联第39条——必要时进行保护性拷贝),这里的注意点和第39条一样:保护性拷贝先于参数有效性检测和避免使用clone方法(但是保护性拷贝会导致这个类需要保护性拷贝的成员不能为final)。 尽管Java1.4中为了阻止恶意攻击并且节省保护性拷贝的开销,在ObjectOutputStream/ObjectInputStream中引入了writeObjectUnshared/readObjectUnshared方法,并且比保护性拷贝更快,但是这些方法可能会受到复杂的攻击,不建议使用。 readObject方法和构造器行为类似,所以对构造器的注意事项同样适用于readObject方法:不要调用可被覆盖的方法。

编写readObject方法的建议:

对于对象引用域必须保持为私有的类,要保护性地拷贝这些域中的每一个对象。不可变类的可变组件就属于这一类 对于任何约束条件,如果检查失败,则抛出InvalidObjectException异常。这些检查动作应该跟在所有的保护性拷贝之后 如果整个对象图在被反序列化之后必须进行检验,就应该使用ObjectInputValidation接口(查了一下,这个接口有方法validateObject,就是用来检验一个有“图”的对象是否符合约束的,验证不成功就抛出2中提到的异常) 无论是直接方式还是间接方式,都不要调用类中任何可能被覆盖的方法。

对于实例控制,枚举类型优先于readResolve

readObject方法实现的困难:

前文中的背景知识提到了readResolve方法,这里再做深化:readResolve的调用是在readObject之后,readResolve方法会返回一个对象,取代readObject反序列化的对象。也就是说存在一种可能,在readResolve调用之前,readObject调用之后,有人恶意地得到反序列化的新的对象,取得它的引用,进而破坏单例。因此需要单例的类的所有实例域都是transient的。 对于readObject,它的可访问性值得考虑,私有意味着这个类失去了被子类化的能力;如果它是受保护或者公有的,而这个类的子类没有覆盖readObject方法,反序列化会产生一个这个类(超类)的实例,可能导致ClassCastException异常

本条建议:鉴于前面提到的诸多困难,建议使用枚举实现单例(实例控制),简单而且不会有差错。但是,如果一个单例的实例在编译时还不能确定(未实例化),那么是无法使用枚举类型的。

考虑用序列化代理代替序列化实例

序列化代理:

在需要序列化的类的内部创建一个私有的静态内部类,这个静态内部类同样实现Serializable接口。静态内部类通过构造函数传入外围类的引用,保留外围类的逻辑状态(比如保留所有数据、约束条件),并且有readObject方法(实现不同,稍后讲到) 同样实现了Serializable接口的外部类需要编写方法writeReplace,返回一个new出来的静态内部类(传入了自己的引用)。wrieReplace会在序列化的时候对写入的对象进行替换,替换为这个静态内部类。 当反序列化的时候,不是调用外围类,而是调用静态内部类的反序列化的方法readResolve,返回一个使用当初保留的外部类的全部信息构造的外部类。

使用序列化代理的好处:

可以像保护性拷贝方法一样阻止伪造字符流的攻击以及内部域的盗用 可以不必像保护性拷贝那样不能把需要把需要拷贝的值设为final 允许反序列化实例与原始序列化实例得到不同的类 无需花费很多形式

使用序列化代理的局限性:

不能与可被客户扩展的类兼容:(我理解是静态内部类没有写入文档,而且不能扩展,如果客户代码新加入了域,这个静态内部类不能保存新加入域的任何信息,进而影响序列化/反序列化的能力) 不能与对象图中包含循环的某些类兼容:因为不能从对象的序列化代理的readResolve方法中调用这个对象的方法,因为这个对象还不存在,可能增加性能开销。

原文  http://wangjinlong.xyz/2018/10/21/Effective_Java_notes10/
正文到此结束
Loading...