Effective Java读书笔记——第十一章 序列化

本文关注序列化API,他提供了一个框架,用来将对象编码城字节流,并从字节流编码中重新构建对象。将一个对象编码成一个字节流,成为将该对象序列化;反之称为反序列化。一旦对象被序列化后,它的编码就可以从一台正在运行的虚拟机被传递到另一太虚拟机上,或者被存储到硬盘上,供以后反序列化时用。


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

想要使一个类可被序列化只需实现Serializable接口,这就存在一个误解,认为任何类毫不犹豫都应该实现序列化。实际情况并非如此。虽然一个类可被实例化的直接开销很低,但为了雪花而长期的开销往往是是实实在在的。

实现Serializable接口的最大代价是,一旦一个类被发布,就大大降低了“改变这个类的实现”的灵活性。如果一个类实现了Serializable接口,它的字节流编码就变成了它导出的API的一部分。一旦这个类被广泛使用,往往必须永远支持这种序列化形式。如果接受了默认的序列化形式,这个类中私有的和包级似有的实例域都将变成导出的API的一部分,这不符合“最低限度地访问域的实践规则;序列化会使类的演变受到限制,这种限制与流的唯一标识符有关(UID,stream unique identifier),每个可序列化的类都有一个唯一的UID,如果没有的实现Serializable接口时设置该UID(它是一个私有的静态的名为serialVersionUID的域),系统会自动地在运行时为你生成一个,这个UID是根据类的名称、实现的接口的名称、所有的公有的和受保护的成员的名称所影响的。如果你在后续版本中对该类有任何改进,如添加了一个方法或是域,那么自动陈胜的UID也会法伤变化。

第二个代价是,实现Serializable接口增加了出现bug和安全漏洞的可能性。

第三个问题是随着发行新的版本,相关的测试负担也增加了。相关测试负担也增加了。

一些值类应该事先Serializable接口,比如Date和BigInteger,大多数集合类也应当如此。而代表活动实体的类,如线程池,不应当实现Serializable。

为继承而设计的类,不应或尽量少的实现Serializable。

内部类不应该是先Serializable。

而静态内部类可以实现Serializable。

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

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

对于一个仅仅表示人名的类,默认的序列化形式就是合理的:

public class Name implements Serializable {
    /**
     *Last name ,必须为非空
     *@serial
     */
    private final String lastName;
    /**
     *First name ,必须为非空
     *@serial
     */
    private final String firstName;
    /**
     *Middle name ,可以为空
     *@serial
     */
    private final String middleName;
}

从逻辑的角度而言,一个名字包含三个字符串,分别表示姓、名、中间名。Name中的实例域精确地反映了它的逻辑内容。

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

而下面的类是个极端:

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;
    }
}

从逻辑上讲,这个列表示一个字符串序列。但从物理角度讲,他表示一个双向链表。

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

  • 它使这个类的导出API(公有方法)永远地束缚在该类的内部表示法上。上面的例子中,私有的StringList.Entry变成了公有API的一部分。

  • 它消耗过多的空间。上述序列化形势表示链表中的每个项,也表示所有的链接关系。但是这是不必要的因为这些链表项的链接只不过是实现细节,并值得记录在序列化的形式中。

  • 它会消耗过多的时间。序列化逻辑并不了解对象图的拓扑关系,所以它必须经过一个昂贵的图遍历过程。上面的额例子中,沿着next引用进行遍历是非常简单的。

  • 它会引起栈溢出。

下面是StringList的一个改进版本:

public final class StringList implements Serializable {
    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){ ... }
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 numElements = s.readInt();
    for (int i = 0;i < numElements; ++i) {
        add((String) s.readObject());
    }

}

}

上面的改进中用到了transient关键字,它表示被修饰的实例域将从以个类的默认序列化形式中省略掉。

尽管StringList的所有域都是transient的,但是writeObject的首要任务是调用defaultWriteObject,而readObject的首要任务是自动阿勇defaultReadObject。如果所有的实例域都是瞬时的,不调用
defaultWriteObject和defaultReadObject也是允许的,但不推荐这样做。即使所有的实例域都是transient的,调用defaultWriteObject也会影响该类的序列化形式,从而极大地增强灵活性。

无论是否使用默认的序列化形式,当defaultWriteObject方法被调用的时候,每一个未被标记为transient的实例域都会被序列化。所以冗余的域都应当被标记。在决定讲一个域做成非transient的之前,请一定要确信它的值将是该对象逻辑状态的一部分。

如果使用的是默认的序列化形式,并且把一个或多个域标记为transient,则要记住当一个实例被反序列化的时候,这些域将被初始化为它们的默认值。

无论是否使用默认的额序列化形式,如果在读取整个对象状态的任何其他方法上强制任何同步,则也必须在对象序列化上强制这种同步。所以如果有一个线程安全的对象,他通过同步每个方法实现了它的线程安全,并且选择使用默认的序列化形式,就要使用下列的writeObject方法:

private synchronized void write(ObjectOutputStream s) throws IOException {
    s.defaultWriteObject(); 
}

如果把同步放在writeObject中,就必须保证遵守与其他动作相同的锁排列约束条件。

另外,不管选择了哪种序列化形式,都要为自己编写的每个可序列化的类声明一个现实的序列版本UID,这样可以避免序列版本UID称为潜在的不兼容的根源:

private static final long serialVersionUID = randomLongValue;

总之,只有当默认的序列化形式能够合理地描述对象的逻辑状态时,才能使用默认的序列化形式;否则就要设计一个自定义的序列化形式,通过它合理地描述对象的状态。

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

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

  • 对于对象引用域必须保持为私有的类,要保护性地拷贝这些域中的每个对象。不可变类的可变组件就属于这一类别。

  • 对于任何约束条件,如果检查失败,则抛出InvalidObjectException异常。这些检查动作应该跟在所有的保护性拷贝之后。

  • 如果整个对象图在被反序列化之后必须进行验证,就应当使用ObjectInoutValidation接口。

  • 无论是直接方式还是间接方式,都不要调用类中任何可被覆盖的方法。


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

对于单例模式:

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

如果加上了implements Serializable,那么这个类就是不在是一个单例了。任何一个readObject方法,不管是显式的还是默认的,都会返回一个新建的实例。

readResolve方法允许用readObject实例代替另一个实例。对于一个正在被反序列化的对象如果它的类定义了一个readResolve方法,那么再反序列化后,新建的对象上的readResolve方法就会被调用。然后该方法返回的对象引用将返回,取代新建对象。

所以如果Elvis类要实现Serializable接口,那么重写readResolve方法就足以保证它的Singleton属性:

private Object readResolve() {
    return INSTANCE;
}

该方法忽略readObject返回的对象,而只返回该类初始化时创建的那个特殊的Elvis实例。实际上,如果依赖readResolve进行实例控制,带有对象引用类型的所有实例域都必须声明为transient的。

如果将一个可序列化的实例受控的类编写成枚举,就可以保证除了所声明的常量之外,不会有别的实例:

public enum Elvis {
    INSTANCE;
    private String[] favorivtSongs = {"Hound Dog", "Heartbreak Hotel"};
    public void printFavorites() {
        System.out.println(Arrays.toString(favoriteSongs));
    }
}

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

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

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


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

略。。。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值