【读书笔记】《Effective Java》(10)--序列化

来源:互联网 发布:招聘网络女主播 编辑:程序博客网 时间:2024/05/21 16:57

这一部分就是《Effective Java》 的最后一章了,读书笔记作完,但是以后还要经常温习,毕竟知识点很多。

序列化

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

74. 谨慎地实现Serializable接口

  • 实现Serializable接口是有代价的:

    1. 一旦一个类实现了Serializable接口并且发布,就大大降低了“改变这个类的实现”的灵活性:

    • 因为这个类的字节流编码成为了它导出的API的一部分,如果采用默认的序列化形式,甚至私有以及包级私有的成员都会导出。这回迫使程序员维护这种序列形式。同时,这也违反了第13条的“最低限度访问域”的建议。
    • 如果接受了默认的序列化形式,并且以后又要改变这个类的内部表示方法,则可能会导致序列化形式的不兼容,虽然在改变内部表示法的同时仍然维持着原来的序列化形式(使用ObjectoutputStream.putFields和ObjectInputStream.readFields)是可能的,但是过程复杂。
    • 序列化还会使类的演变收到限制,比如说受到序列版本UID(serial version UDI)的限制。如果不显式指定一个UID,系统会自动生成它,生成的UID受到类的名称、实现的接口的名称、以及所有公有的和受保护的成员的名称的影响。如果后期向这个类中增加或者删减了任何东西,都有可能导致这个自动生成的UID的变化,从而影响到这个类序列化/反序列化是否成功。
    • 增加了出现Bug和安全漏洞的可能性:反序列化就像是一个隐藏的构造器一样,这个过程中同样需要有像构造器一样建立起该类的各种条件约束,但这在反序列化中容易忘记。
    • 随着类的版本更新,对实现了Serializable接口的类的测试负担会加重:每一次版本变化都要测试当前版本的类到以前版本的类的序列化/反序列化是否可以兼容。除了二进制兼容,还要语义兼容。
  • 什么时候实现Serializable接口?

    1. 一般来说值类应该实现,而代表活动实体的类不应该实现
    2. 为了继承而设计的类应该尽可能少地实现,但是这个类的子类也许需要实现Serializable接口。这种情况下需要这个专门为继承而设计的类提供一个无参的构造器。提供无参构造器一般来说是容易的,但是如果这个为了继承而设计的类是有状态的(也就是说不可能无参数),这时提供无参构造器比较复杂。推荐的做法是在无参构造器之外再提供额外的初始化方法和状态检测方法,调用其他方法前要有初始化,每次进入其他方法还要状态检测。
    3. 内部类不应该实现序列化,因为它的默认序列化定义不清楚,而静态成员类是可以实现Serializable接口序列化的。

  • 75. 考虑使用自定义的序列化形式

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

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

      1. 它使这个类的导出API永远束缚在该类的内部表示法上
      2. 它会消耗过多空间
      3. 它会消耗过多时间
      4. 它会引起栈溢出(在一个对象的物理表示法使用链表,需要对链表进行遍历的时候)
    • 本条建议:

      1. 使用transient关键字标记要从默认序列化形式中省略的成员,然后在writeObject中对它们进行自定义操作,同时编写readObject方法。
      2. 对于实现了自定义序列化形式的类,使用JavaDoc进行注释,尽管readObject/writeObject方法是私有的也是如此。这里需要使用标签@serial@serialData
      3. 自定义序列版本UID(serial version UID),这样会避免它成为不兼容的原因,同时还会提高性能。
      4. 如果自定义序列化形式的类的其他有关状态更改的方法使用了同步,那么在对象序列化上也要同步。

    76. 保护性地编写readObject方法

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

    • 本条建议:

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

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

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

    • readObject方法实现的困难:

      1. 前文中的背景知识提到了readResolve方法,这里再做深化:readResolve的调用是在readObject之后,readResolve方法会返回一个对象,取代readObject反序列化的对象。也就是说存在一种可能,在readResolve调用之前,readObject调用之后,有人恶意地得到反序列化的新的对象,取得它的引用,进而破坏单例。因此需要单例的类的所有实例域都是transient的。
      2. 对于readObject,它的可访问性值得考虑,私有意味着这个类失去了被子类化的能力;如果它是受保护或者公有的,而这个类的子类没有覆盖readObject方法,反序列化会产生一个这个类(超类)的实例,可能导致ClassCastException异常
    • 本条建议:鉴于前面提到的诸多困难,建议使用枚举实现单例(实例控制),简单而且不会有差错。但是,如果一个单例的实例在编译时还不能确定(未实例化),那么是无法使用枚举类型的。


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

    • 序列化代理:

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

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

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

    0 0
    原创粉丝点击