Contents

Effective Java 2 Serialization

对象序列化(object serialization):将对象编码成字节流(序列化),并从字节流编码中重新构建对象(反序列化)

一旦对象被序列化后,它的编码就可以在机器之间传递,或者被存储到磁盘上,供以后反序列化使用。

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

要使一个类可被实例化,在声明中加入implements Serializable即可。然后实现序列化需要代价的,如下:

代价

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

如果一个类实现了Serializable接口,它的字节流编码就变成了它的导出的API的一部分。也就是说,如果你使用了默认的序列化形式,这个类中私有的和包级私有的实例域将都变成导出的API的一部分,从而失去了信息隐藏工具的有效性。

如果你接受了默认序列化形式,并且以后又要改变这个类的内部表示法,结果可能导致序列化形式的不兼容。即client用旧版本序列化,用新版本反序列化,结果导致失败。

序列化会使类的演变受到限制。每个可序列化的类都有一个唯一标识号与它相关联,称作序列版本UID(serial version UID)。如果你没有显示的声明该标识号,系统会自动地根据这个类来调用一个复杂运算过程来产生标识号。所以,如果你增加了一个工具方法,自动产生的UID也会变化,从而导致兼容性遭到破坏,产生InvalidClassException异常。

代价2:

它增加了出现Bug和安全漏洞的可能性。

序列化机制是一种语言之外的对象创建机制。无论你是接受了默认的行为,还是覆盖了默认的行为,反序列化机制都是一个"隐藏的构造器”,具备与其他构造器相同的特点。

因为反序列化机制中没有显示的构造器,所以很容易忘记:反序列化过程必须要保证所有"由真正的构造器建立起来的约束关系”,并且不允许攻击者访问正在构造过程中的对象的内部信息,而依靠默认的反序列化机制很容易遭到破坏。

代价3:

随着类发行新的版本,相关的测试负担也增加了

很容易理解,新版本和旧版本之间的序列化与反序列化,必须要保证测试成功。

益处

如果一个类将要加入到某个框架中,并且该框架依赖于序列化来实现对象传输或者持久化。 实现Serializable接口就非常有必要。

如HttpServlet实现序列化,因此会话状态(session state)可以被缓存。Throwable实现序列化,所以RMI异常可以从服务器传到客户端。

然而一般情况,为了继承而设计的类应该尽可能少地去实现Serializable接口,用户接口也是。(但是上面情况下就是可以的,因为它们参与到需要持久化的框架中了)

如果一个专门为了继承而设计的类不是可序列化的,就不可能编写出可序列化的子类。特别的是,如果超类没有提供可访问的无参构造器,子类也不可能做到可序列化。因此,对于为继承设计的不可序列化的类,你应该考虑提供一个无参构造器

如下增加了一个protected的无参构造器和一个初始方法。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
public abstract class AbstractFoo {
    private int x, y;
    private enum State {NEW, INITIALIZING, INITIALIZED};
    private final AtomicReference<State> init =
            new AtomicReference<State>(State.NEW);

    public AbstractFoo(int x, int y) { initialize(x, y); }

    //允许子类的readObject方法初始化state
    protected AbstractFoo() {    }
    protected final void initialize(int x, int y){
        if (!init.compareAndSet(State.NEW, State.INITIALIZING))
            throw new IllegalStateException("Already initilized");
        this.x = x;
        this.y = y;
        init.set(State.INITIALIZED);
    }
    //提供保护性的访问以便子类的writeObject
    protected int getX() { checkInit(); return x; }
    protected int getY() { checkInit(); return y; }
    private void checkInit() {
        if (init.get() != State.INITIALIZED)
            throw new IllegalStateException("Uninitialized");
    }
}

注意init域是一个原子引用,确保特定情况时,对象的完整性。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
public class Foo extends AbstractFoo implements Serializable {
    private static final long serialVersionUID = 6743047006166517852L;

    private void readObject(ObjectInputStream s) throws IOException, ClassNotFoundException {
        s.defaultReadObject();
        int x = s.readInt();
        int y = s.readInt();
        initialize(x, y);
    }
    private void writeObject(ObjectOutputStream s) throws IOException {
        s.defaultWriteObject();
        s.writeInt(getX());
        s.writeInt(getY());
    }

    public Foo(int x, int y){
        super(x, y);
    }
}

以下是测试:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
@Test
public void SerializableTest(){
    Foo writeFoo = new Foo(1,2);
    Foo readFoo = null;
    try {
        ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("Foo.out"));
        oos.writeObject(writeFoo);
        oos.close();

        ObjectInputStream ois = new ObjectInputStream(new FileInputStream("Foo.out"));
        readFoo = (Foo)ois.readObject();
        ois.close();
    } catch (FileNotFoundException e) {
        e.printStackTrace();
    } catch (IOException e) {
        e.printStackTrace();
    } catch (ClassNotFoundException e) {
        e.printStackTrace();
    }

    assertEquals(readFoo.getX(), 1);
    assertEquals(readFoo.getY(), 2);
}

内部类不应该实现Serializable。它们使用编译器产生的合成域来保存指向外围实例的引用,以及保存来自外围作用域的局部变量的值

内部类的默认序列化形式是定义不清楚的,然而静态成员类却可以实现Serializable接口。

总而言之,实现Serializable要小心。如果一个类为了继承而设计的,在允许/禁止 子类实现Serializable接口之间,提供一个可访问的无参构造器是可选折中方案。

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

1.首先什么是 默认的序列化形式:

默认的序列化形式描述了该对象内部所包含的数据,以及每一个可以从这个对象到达的其他对象的内部数据。它也描述了所有这些对象被链接起来后的拓扑结构。

2.对一个对象来说,序列化形式包括它的逻辑数据和物理表示法

如果一个对象的物理表示法 等同于 它的逻辑内容,可能就适合默认序列化形式,但通常还提供一个readObject保证约束关系和安全性。

如下Name类,从逻辑上看 一个Name包含2个字符串。 同时,在物理表示上,Name类通过2个实例域精确的反映了它的逻辑内容。

1
2
3
4
public class Name implements Serializable{
	private final String lastName;
	private final String firstName;	
}

如果一个对象的物理表示法 与 它的逻辑内容 有实质性的区别时,默认序列化就存在问题

如下StringList类,从逻辑上看 它表示一个字符串列表。 但是,在物理表示上,它表示一个双向链表。

此时,采用默认序列化形式,该序列化会镜像出链表中的所有项,以及这些项之间的双向链接

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
public class SringList implements Serializable{
    private int size = 0;
    private Entry head = null;

    private static class Entry implements Serializable{
        String data;
        Entry next;
        Entry previous;
    }
}

所以,此时的默认序列化存在以下问题:

  • 它使这个类导出API永远地束缚在该类的内部表示法上 private的Entry类变成公有API的一部分,对将来维护改变产生问题。
  • 它会消耗过多的空间 如上的StringList一样,默认序列化形式过于庞大,不利于写到磁盘或者在网络中传输
  • 它会消耗过多的时间 序列化逻辑并不了解对象图的拓扑关系,所以它必须要经过一个昂贵的图遍历
  • 它会引起栈溢出 默认的序列化过程要对对象图执行一次递归遍历,很可能引起栈溢出

因此,解决方法包含writeObject和readObject

如下:transient表明这个实例域将从一个类的默认序列化形式中省略掉

虽然以下的所有域都是瞬时的(transient),但是不推荐省略defaultWriteObject()。如果以后版本加入了非瞬时的域,却没有defaultWriteObject(),就造成反序列化失败。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
public class SringList implements Serializable{
    private transient int size = 0;
    private transient Entry head = null;

    private static class Entry{
        String data;
        Entry next;
        Entry previous;
    }
    //Appends the data to the list
    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());
    }
}

无论是否使用默认的序列化形式,当调用defaultWriteObject的时候,每一个未被标记为transient的实例域都会被序列化。因此要确定这些域是否是该对象逻辑状态的一部分。

如果使用默认序列化形式,被标记transient的实例,在反序列化的时候,这些域将被初始化为它们的默认值(Java类型的default value)

如果在读取读取整个对象状态的任何其他方法上强制任何同步,则必须在对象的序列化上强制这种同步。

不管选择哪种序列化形式,都要为自己编写的每个可序列化的类声明一个显示的序列版本UID,如:

1
private static final long serialVersionUID = randomLongValue;

总而言之,当将一个类序列化的时候,要仔细考虑采用什么样的序列化形式

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

正如第39条:必要时进行保护性拷贝。

  • readObject方法实际上相当于另一个公有的构造器,同其他构造器一样,构造器必须检查其参数的有效性,必要时进行保护性拷贝。
  • readObject是一个用字节流作为唯一参数的构造器,正常情况下,对一个正常构造的实例进行序列化可以产生字节流。

当一个对象被反序列化的时候,对于客户端不应该拥有对象的引用,如果哪个域包含了这样的对象引用,就必须要做保护性拷贝。

对于非final的可序列化的类,readObject方法不可以调用可被覆盖的方法,因为被覆盖的方法将在子类的状态被反序列化之前先运行。程序很可能失败。

总而言之,对于编写readObject的时候,设想成正在编写一个公有的构造器,无论给它传递什么样的字节流,它都必须产生一个有效的实例。不要假设这个字节流一定代表着一个真正被序列化过的实例 以下是编写readObject的指导方针:

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

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

对于一个Singleton的类,如果继承了Serializable,它就不再是一个Singleton。因为任何一个readObject方法,不管显示还是默认的,都会返回一个新建的实例。这个新建的实例不同于该类初始化时创建的实例。

readResolve:反序列化之后,新建对象上的readResolve方法就会被调用。然后该方法返回的对象引用将被返回 取代新建的对象 如下返回一个单例:

1
private Object readResolve(){	return INSTANCE;	}

注意:如果依赖readResolve进行实例控制,带有对象引用类型的所有实例域则都必须声明为transient,否则攻击者就可以在readResolve方法被运行之前,反序列化出实例域。当对象引用域的内容被反序列化时,它就允许一个"盗用流"指向最初被反序列化的Singleton的引用。此时,反序列化出的Singleton就不再是之前的Singleton

自从Java1.5以后,建议使用enum来实现Singleton。枚举实现了序列化,详见

注意使用枚举反序列化出来的Singleton,和原来的Singleton是完全一致的。它们的hashCode是一样的

总而言之,尽可能使用枚举实施实例控制的约束条件。如果做不到,则提供一个readResolve方法,并确保该类的所有实例域都为基本类型,或者是transient。

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

序列化代理:为可序列化的类设计一个私有的静态嵌套类,精确地表示外围类的实例的逻辑状态。这个类称作序列化代理。

它应该有一个单独的构造器,其参数类型就是那个外围类。这个构造器只从它的参数中复制数据:它不需要进行任何一致性检查或者保护性拷贝 如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
public class OutClass implements Serializable{
    private final Date start;
    private final Date end;
    public OutClass(Date start, Date end) {
        this.start = start;
        this.end = end;
    }

    public Date getStart() { return new Date(start.getTime()); }
    public Date getEnd() { return new Date(end.getTime()); }

    private static class SerializationProxy implements Serializable{
        private static final long serialVersionUID = 1857109453119975659L;
        private final Date start;
        private final Date end;
        public SerializationProxy(OutClass o) {
            this.start = o.start;
            this.end = o.end;
        }
        /**
         * 它在逻辑上相当于:反序列化时,将序列化代理转变成外围类的实例
         */
        private Object readResolve(){
            return new OutClass(start, end);
        }
    }

    /**
     * 在序列化之前,将外围类的实例转变成它的序列化代理
     */
    private Object writeReplace(){
        return new SerializationProxy(this);
    }

    /**
     * 防止攻击者仿造外围类的序列化实例
     */
    private void readObject() throws InvalidObjectException {
        throw new InvalidObjectException("Proxy required");
    }
}

主要注意:writeReplace和readResolve这2个方法的作用

当你发现自己必须在一个不能被客户端扩展的类上编写readObject或writeObject时候。或者想要将带有重要约束条件的对象序列化(如Date),就可以采用序列化代理模式