EffectiveJava第11章-序列化

对象序列化:将一个对象编码成字节流。
反之,成为对象反序列化。

第74条:谨慎地实现Serializable接口

实现Serializable接口而付出的最大代价是,一旦一个类被发布,就大大降低了“改变这个类的实现”的灵活性。

如果一个类实现了Serializable接口,它的字节流编码(或者说序列化形式)就变成了它导出的API的一部分。之后的版本进行反序列的时候必须要兼容老的版本。

序列化会使类的演变受到限制

1.序列版本UID必须保持一致,如果没有显式地指定,系统就会自动根据这个类来调用一个负杂的运算过程来产生UID,类的任何改动都有可能改变UID,因此建议显示地指定,并且保持一致。
2.出现bug和安全漏洞的可能性增加。反序列化过程必须也要保证所有“由真正的构造器建立起来的约束条件”,
3.随着类发行新的版本,相关的测试负担也增加了。当类被修订的时候,需要检查是否可以“在新版本中序列化一个实例,然后再旧版本中反序列化”。反之亦然。

为了继承而设计的类应该少实现Serializable接口,用户的接口也应该少继承Serialzable接口。

如果一个类有些约束条件,当类的实例化被初始化成它们的默认值,就会违背这些约束条件,这时候,你就必须给这个类添加这个readObjectNoData方法。

private viod readObjectNoData throws InvalidObjectException{
    throw new InvalidObjectException("stream data required");
}

如果一个专门为了继承而设计的类不是可序列化的,就不可能编写出可序列化的子类。对于为了继承而设计的不可序列化的类,你应该考虑提供一个无参构造器。

内部类不应该实现Serializable。

第75条:考虑使用自定义的序列化形式

设计一个类的序列化形式和设计该类的API 同样重要,因此在没有认真考虑好默认的序列化形式是否合适之前,不要贸然使用默认的序列化行为。在作出决定之前,你需要从灵活性、性能和正确性多个角度对这种编码形式进行考察。一般来讲,只有当你自行设计的自定义序列化形式与默认的形式基本相同时,才能接受默认的序列化形式。

对于一个对象来说,理想的序列化形式应该只包含该对象所表示的逻辑数据,而逻辑数据与物理表示法应该是各自独立的。如果一个对象的物理表示法等同于它的逻辑内容,可能就适用于使用默认的序列化形式。

如果物理表示法和逻辑数据内容有实质性的区别时,使用默认序列化形式会有以下几个缺点:
(1)它使这个类的导出API 永远的束缚在该类的内部表示法上,即使今后找到更好的的实现方式,也无法摆脱原有的实现方式。
(2)它会消耗过多的空间。
(3)它会消耗过多的时间。
(4)它会引起栈溢出。默认的序列化过程要对对象图执行一次递归遍历。

transient是Java语言的关键字,用来表示一个域不是该对象序列化的一部分。当一个对象被序列化的时候,transient型变量的值不包括在序列化的表示中,然而非transient型的变量是被包括进去的。

在序列化过程中,虚拟机会试图调用对象类里的writeObject() 和readObject(),进行用户自定义的序列化和反序列化,如果没有则调用ObjectOutputStream.defaultWriteObject() 和ObjectInputStream.defaultReadObject()。

同样,在ObjectOutputStream和ObjectInputStream中最重要的方法也是writeObject() 和 readObject(),递归地写出/读入byte。

对于默认序列化还需要进一步说明的是,当一个或多个域字段被标记为transient 时,如果要进行反序列化,这些域字段都将被初始化为其类型默认值,如对象引用域被置为null,数值基本域的默认值为0,boolean域的默认值为false。如果这些值不能被任何transient 域所接受,你就必须提供一个readObject方法。它首先调用defaultReadObject,然后再把这些transient 域恢复为可接受的值。

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

第76条:保护性地编写readObject方法

readObject方法实际上相当于另一个公有的构造器,如同其他的构造器一样,它也要求注意同样的所有注意事项:

构造器必须检查其参数的有效性
在必要的时候对参数进行保护性拷贝

不严格地讲,readObject是一个“用字节流作为唯一参数”的构造器。在readObject方法中,必须要保证上述两个注意事项。

总而言之,每当你编写readObject方法的时候,都要这样想:你正在编写一个公有的构造器,无论给它传递什么样的字节流,它都必须产生一个有效的实例。不要假设这个字节流一定代表着一个真正被序列化过的实例。虽然在本条目的例子中,类使用了默认的序列化形式,但是,所有讨论到的有可能发生的问题也同样适用于使用自定义序列化形式的类。下面以摘要的形式给出一些指导方针,有助于编写出更加健壮的readObject方法:

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

第77条:对于实例控制,枚举类型优先于readResolve

public class Elvis {
    public static final Elvis INSTANCE = new Elvis();
    private Elvis() { ... }
    public void leaveTheBuilding() { ... }
}

对于上述Singleton类,如果在类的声明中加上了“implements Serializable”,它就不再是个Singleton。无论该类使用了默认的序列化形式,还是自定义的序列化形式,都没有关系;也跟它是否提供了显式的readObject方法无关。任何一个readObject方法,不管是显式的还是默认的,它都会返回一个新建的实例,这个新建的实例不同于该类初始化时创建的实例。

readResolve特性允许你用readObject创建的实例代替另一个实例[Serialization, 3.7]。对于一个正在被反序列化的对象,如果它的类定义了一个readResolve方法,并且具备正确的声明,那么在反序列化之后,新建对象上的readResolve方法就会被调用。然后,该方法返回的对象引用将被返回,取代新建的对象。在这个特性的绝大多数用法中,指向新建对象的引用不需要再被保留,因此立即成为垃圾回收的对象。

// readResolve for instance control - you can do better!
private Object readResolve() {
    // Return the one true Elvis and let the garbage collector
    // take care of the Elvis impersonator.
    return INSTANCE;
}

该方法忽略了被反序列化的对象,只返回该类初始化时创建的那个特殊的Elvis实例。因此,Elvis实例的序列化形式并不需要包含任何实际的数据;所有的实例域都应该被声明为transient的。事实上,如果依赖readResolve进行实例控制,带有对象引用类型的所有实例域则都必须声明为transient的

如果反过来,你将一个可序列化的实例受控的类编写成枚举,就可以绝对保证除了所声明的常量之外,不会有别的实例。JVM对此提供了保障,这一点你可以确信无疑。

public enum Elvis{
    INSTANCE;
    private String[] favoriteSongs = {"str1","str2"};
    public void printFavorites(){
        ...
    }
}

用readResolve进行实例控制并不过时。如果必须编写可序列化的实例受控的类,它的实例在编译时还不知道,你就无法将类表示成一个枚举类型。

readResolve的可访问性(accessibility)很重要。如果把readResolve方法放在一个final类上,它就应该是私有的。如果把readResolver方法放在一个非final的类上,就必须认真考虑它的可访问性。如果它是私有的,就不适用于任何子类。如果它是包级私有的,就只适用于同一个包中的子类。如果它是受保护的或者公有的,就适用于所有没有覆盖它的子类。如果readResolve方法是受保护的或者公有的,并且子类没有覆盖它,对序列化过的子类实例进行反序列化,就会产生一个超类实例,这样有可能导致ClassCastException异常。

总而言之,你应该尽可能地使用枚举类型来实施实例控制的约束条件。如果做不到,同时又需要一个既可序列化又是实例受控的类,就必须提供一个readResolver方法,并确保该类的所有实例域都为基本类型,或者是transient的。

第78条:考虑用序列化代理代替序列化实例

序列化代理模式相当简单。首先,为可序列化的类设计一个私有的静态嵌套类,精确地表示外围类的实例的逻辑状态。这个嵌套类被称作序列化代理(serialization proxy),它应该有一个单独的构造器,其参数类型就是那个外围类。这个构造器只从它的参数中复制数据:它不需要进行任何一致性检查或者保护性拷贝。按设计,序列代理的默认序列化形式是外围类最好的序列化形式。外围类及其序列代理都必须声明实现Serializable接口。

具体demo可以参考jdk中EnumSet代码

序列化代理模式有两个局限性:

它不能与可以被客户端扩展的类兼容。它也不能与对象图中包含循环的某些类兼容:如果你企图从一个对象的序列化代理的readResolve方法内部调用这个对象中的方法,就会得到一个ClassCastException异常,因为你还没有这个对象,只有它的序列化代理。

最后,序列化代理模式所增强的功能和安全性并不是没有代价的。在我的机器上,通过序列化代理来序列化和反序列化Period实例的开销,比用保护性拷贝进行的开销增加了14%。

总而言之,每当你发现自己必须在一个不能被客户端扩展的类上编写readObject或者writeObject方法的时候,就应该考虑使用序列化代理模式。要想稳健地将带有重要约束条件的对象序列化时,这种模式可能是最容易的方法。

总结:

当父类继承了Serializable接口时,所有子类都可以被序列化。
子类实现了Serializable接口,父类没有,父类中的属性不能被序列化(不报错,数据会丢失),但是在子类中属性仍能正确序列化。
如果序列化的属性是对象,则这个对象也必须实现Serializable接口,否则会报错。
在反序列化时,如果对象的属性有修改或删减,则修改的部分属性会丢失,但不会报错。
在反序列化时,如果serialVersionUID被修改,则反序列时会失败。

你可能感兴趣的:(EffectiveJava第11章-序列化)