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

序列化

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

实现Serializable接口而付出的最大代价是,一旦一个类被发布,就大大降低了“改变这个类的实现”的灵活性,并且以后又要改变这个类的内部表示法,结果可能导致序列化形式的不兼容

序列化会使类的演变受到限制,这种限制的一个例子与流的唯一标识符有关,通常它也被称为序列版本UID。如果没有在一个名为serialVersionUID的私有静态final的long域中显式地指定该标识号,系统就会自动地根据这个类来调用一个复杂的运算过程,从而在运行时产生该标识号。这个自动产生的值会受到类名称、它所实现的接口的名称、以及所有公有的和受保护的成员的名称所影响,如果通过任何方式改变了这些信息,自动产生的序列版本UID也会发生变化,在运行时导致InvalidClassException异常

实现Serializable的第二个代价是,它增加了出现Bug和安全漏洞的可能性。序列化机制是一种语言之外的对象创建机制,依靠默认的反序列化机制,很容易使对象的约束关系遭到破坏,以及遭受到非法访问

实现Serializable的第三个代价是,随着类发行新的版本,相关的测试负担也增加了,因为要检查是否可以“在新版本中序列化一个实例,然后在旧版本中反序列化”,反之亦然

为了继承而设计的类应该尽可能少地去实现Serializable接口,用户的接口也应该尽可能少地继承Serializable接口,如果一个类或者接口存在的目的主要是为了参与到某个框架中,该框架要求所有的参与者都必须实现Serializable接口,那么,对于这个类或者接口来说,实现或者扩展Serializable接口就是非常有意义的

在“允许子类实现Serializable接口”或“禁止子类实现Serializable接口”两者之间的一个折衷设计方案是,提供一个可访问的无参构造器,这种设计方案允许(但不要求)子类实现Serializable接口

public abstract class AbstractFoo {
    private int x, y;  // Our state

    // This enum and field are used to track initialization
    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); }

    // This constructor and the following method allow
    // subclass's readObject method to initialize our state.
    protected AbstractFoo() { }

    protected final void initialize(int x, int y) {
        if (!init.compareAndSet(State.NEW, State.INITIALIZING))
            throw new IllegalStateException(
                "Already initialized");
        this.x = x;
        this.y = y;
        // Do anything else the original constructor did
        init.set(State.INITIALIZED);
    }

     // These methods provide access to internal state so it can
     // be manually serialized by subclass's writeObject method.
    protected final int getX() { checkInit(); return x; }
    protected final int getY() { checkInit(); return y; }
    // Must call from all public and protected instance methods
    private void checkInit() {
        if (init.get() != State.INITIALIZED)
            throw new IllegalStateException("Uninitialized");
    }
    // Remainder omitted
}

AbstractFoo中所有公有的和受保护的实例方法在开始做任何其他工作之前都必须先调用checkInit,这样可以确保如果有编写不好的子类没有初始化实例,该方法调用就可以快速而干净地失败

public class Foo extends AbstractFoo implements Serializable {
    private void readObject(ObjectInputStream s)
            throws IOException, ClassNotFoundException {
        s.defaultReadObject();

        // Manually deserialize and initialize superclass state
        int x = s.readInt();
        int y = s.readInt();
        initialize(x, y);
    }

    private void writeObject(ObjectOutputStream s)
            throws IOException {
        s.defaultWriteObject();

        // Manually serialize superclass state
        s.writeInt(getX());
        s.writeInt(getY());
    }

    // Constructor does not use the fancy mechanism
    public Foo(int x, int y) { super(x, y); }

    private static final long serialVersionUID = 1856835860954L;
}

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

如果没有先认真考虑默认的序列化形式是否合适,则不要贸然接受,如果确定了默认的序列化形式是合适的,通常还必须提供一个readObject方法以保证约束关系和安全性

自定义的序列化示例如下:

public final class StringList implements Serializable {
    // transient修饰符表明这个实例域将从一个类的默认序列化形式中省略掉
    private transient int size   = 0;
    private transient Entry head = null;

    // No longer Serializable!
    private static class Entry {
        String data;
        Entry  next;
        Entry  previous;
    }

    // Appends the specified string to the list
    public final void add(String s) {
        // Implementation omitted
    }

    /**
     * Serialize this {@code StringList} instance.
     *
     * @serialData The size of the list (the number of strings
     * it contains) is emitted ({@code int}), followed by all of
     * its elements (each a {@code String}), in the proper
     * sequence.
     */
    private void writeObject(ObjectOutputStream s)
            throws IOException {
        s.defaultWriteObject();
        s.writeInt(size);

        // Write out all elements in the proper order.
        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();

        // Read in all elements and insert them in list
        for (int i = 0; i < numElements; i++)
            add((String) s.readObject());
    }

    private static final long serialVersionUID = 93248094385L;
    // Remainder omitted
}

注意,writeObject方法和readObject方法的首要任务都应该是调用defaultWriteObject和defaultReadObject,这样得到的序列化形式允许在以后的发行版本中增加非transient的实例域,并且还能保持向前或者向后兼容性

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

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

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

不管选择了哪种序列化形式,都要为自己编写的每个可序列化的类声明一个显式的序列版本UID,避免序列版本UID成为潜在的不兼容根源

private static final long serialVersionUID = randomLongValue;

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

在readObject方法中首先调用defaultReadObject,然后检查被反序列化之后的对象的有效性,如果有效性检查失败,就抛出一个InvalidObjectException异常,使反序列化过程不能成功地完成,这样做的目的是避免攻击者创建无效的实例

private void readObject(ObjectInputStream s) throws IOException, ClassNotFoundException {
    s.defaultReadObject();

    // Check that out invariants are satisfied
    if(start.compareTo(end) > 0) {
        throw new InvalidObjectException(start + " after " + end);
    }
}

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

private void readObject(ObjectInputStream s) throws IOException, ClassNotFoundException {
    s.defaultReadObject();

    // Defensively copy our mutable components
    start = new Date(start.getTime());
    end = new Date(end.getTime());

    // Check that out invariants are satisfied
    if(start.compareTo(end) > 0) {
        throw new InvalidObjectException(start + " after " + end);
    }
}

注意,保护性拷贝是在有效性检查之前进行的

有一个简单的“石蕊”测试,可以用来确定默认的readObject方法是否被接受,测试方法:增加一个公有的构造器,其参数对应于该对象中每个非transient的域,并且无论参数的值是什么,都是不进行检查就可以保存到相应的域中的,如果做不到,就必须提供一个显式的readObject方法,并且它必须执行构造器所要求的所有有效性检查和保护性拷贝

下面的指导方针有助于编写出更加健壮的readObject方法:

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

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

任何一个readObject方法,不管是显式的还是默认的,它都会返回一个新建的实例,这个新建的实例不同于该类初始化时创建的实例

readResolve特性允许用readObject创建的实例代替另一个实例,对于一个正在被反序列化的对象,如果它的类定义了一个readResolve方法,并且具备正确的声明,那么在反序列化之后,新建对象上的readResolve方法就会被调用。然后,该方法返回的对象引用将被返回,取代新建的对象。如下的readResolve方法保证了Elvis类的Singleton属性:

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

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

    private Object readResolve() throws ObjectStreamException {
        return INSTANCE;
    }
}

该方法忽略了被反序列化的对象,只返回该类初始化时创建的那个特殊的Elvis实例,因此,Elvis实例的序列化形式并不需要包含任何实际的数据,所有的实例域都应该被声明为transient的

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

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

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

public final class Period implements Serializable {
    private final Date start;
    private final Date end;

    /**
     * @param  start the beginning of the period
     * @param  end the end of the period; must not precede start
     * @throws IllegalArgumentException if start is after end
     * @throws NullPointerException if start or end is null
     */
    public Period(Date start, Date end) {
        this.start = new Date(start.getTime());
        this.end   = new Date(end.getTime());
        if (this.start.compareTo(this.end) > 0)
            throw new IllegalArgumentException(
                          start + " after " + end);
    }

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

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

    public String toString() { return start + " - " + end; }

    // Serialization proxy for Period class
    private static class SerializationProxy implements Serializable {
        private final Date start;
        private final Date end;

        SerializationProxy(Period p) {
            this.start = p.start;
            this.end = p.end;
        }

        private static final long serialVersionUID = 
            234098243823485285L; // Any number will do (Item 75)

        // readResolve method for Period.SerializationProxy
        private Object readResolve() {
            return new Period(start, end);  // Uses public constructor
        }
    }

    // writeReplace method for the serialization proxy pattern
    private Object writeReplace() {
        return new SerializationProxy(this);
    }

    // readObject method for the serialization proxy pattern
    private void readObject(ObjectInputStream stream) 
            throws InvalidObjectException {
        throw new InvalidObjectException("Proxy required");
    }
}

通过writeReplace方法,产生一个SerializationProxy实例,代替外围类的实例,有了writeReplace方法,序列化系统永远不会产生外围类的序列化实例,最后,在SerializationProxy类中提供一个readResolve方法,它返回一个逻辑上相当的外围类的实例。这个方法的出现,导致序列化系统在反序列化时将序列化代理转变回外围类的实例

序列化代理方法可以阻止伪字节流的攻击以及内部域的盗用攻击,这种方法不必要知道哪些域可能受到狡猾的序列化攻击的威胁,也不必显式地执行有效性检查

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

总之,每当必须在一个不能被客户端扩展的类上编写readObject或者writeObject方法的时候,就应该考虑使用序列化代理模式

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值