【10】Effective Java - 序列化

1、谨慎地实现Serializable接口

(1)序列化和反序列化

     将一个对象编码成一个字节流,称为序列化(serializing);相反的处理过程,称之为反序列化(deserializing)

(2)实现Serializable接口的代价
A、降低灵活性    

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

    客户端程序企图用这个类的旧版本来序列化一个类,然后用新版本进行反序列化,结果将导致程序失败。

    序列化版本(serial version UID),每个序列化的类都有一个唯一标识号与它相关联,如果没有一个静态私有final的long域显示指定该标识号,系统会自动地根据这个类来调用一个复杂的运算过程,从而在运行时产生标识号。


B、增加出现bug和安全漏洞的可能性

     序列化是一种语言之外的对象创建机制,反序列化是一个隐藏的构造器,它容易使对象的约束关系遭到破坏,以及遭到非法访问。


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

     需要确保序列化反序列化过程的成功,也要确保结果产生的对象真正是原始对象的复制品。


(3)继承类和接口少使用序列化接口

     根据经验,Date类和BigInteger这样的值类应该实现序列化接口(大多数集合类也应该如此)。

     对于为继承而设计的不可序列化的类,应该考虑提供一个无参构造器。


(4)内部类不该实现序列化接口,而静态成员类可以

    内部类使用编译器产生的合成域来保存指向外围实例的引用,以及保存来自外围作用域的局部变量的值。内部类的默认序列化形式是定义不清楚的,而静态成员类可以。


2、考虑使用自定义的序列化形式

(1)何时使用默认的序列化形式

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

   通常还必须提供一个readObject方法以保证约束关系和安全性。


(2)当物理表示法与逻辑表示有实质区别时

    此时使用默认序列化有如下缺点:

A、导出的API永远束缚在该类的内部表示法上

B、消耗过多空间

C、消耗过多时间

D、会引起栈溢出


(3)自定义序列化方法
// StringList with a reasonable custom serialized form

import java.io.*;

public final class StringList implements Serializable {
    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
}


3、保护性地编写readObject方法

(1)保护性拷贝
public final class Period implements Serializable {
    private Date start;
    private 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; }

}


(2)人工伪造字节流,破坏约束条件
public class BogusPeriod {
    // Byte stream could not have come from real Period instance!
    private static final byte[] serializedForm = new byte[] {
     (byte)0xac, (byte)0xed, 0x00, 0x05, 0x73, 0x72, 0x00, 0x06,
     0x50, 0x65, 0x72, 0x69, 0x6f, 0x64, 0x40, 0x7e, (byte)0xf8,
     0x2b, 0x4f, 0x46, (byte)0xc0, (byte)0xf4, 0x02, 0x00, 0x02,
     0x4c, 0x00, 0x03, 0x65, 0x6e, 0x64, 0x74, 0x00, 0x10, 0x4c,
     0x6a, 0x61, 0x76, 0x61, 0x2f, 0x75, 0x74, 0x69, 0x6c, 0x2f,
     0x44, 0x61, 0x74, 0x65, 0x3b, 0x4c, 0x00, 0x05, 0x73, 0x74,
     0x61, 0x72, 0x74, 0x71, 0x00, 0x7e, 0x00, 0x01, 0x78, 0x70,
     0x73, 0x72, 0x00, 0x0e, 0x6a, 0x61, 0x76, 0x61, 0x2e, 0x75,
     0x74, 0x69, 0x6c, 0x2e, 0x44, 0x61, 0x74, 0x65, 0x68, 0x6a,
     (byte)0x81, 0x01, 0x4b, 0x59, 0x74, 0x19, 0x03, 0x00, 0x00,
     0x78, 0x70, 0x77, 0x08, 0x00, 0x00, 0x00, 0x66, (byte)0xdf,
     0x6e, 0x1e, 0x00, 0x78, 0x73, 0x71, 0x00, 0x7e, 0x00, 0x03,
     0x77, 0x08, 0x00, 0x00, 0x00, (byte)0xd5, 0x17, 0x69, 0x22,
     0x00, 0x78 };

    public static void main(String[] args) {
        Period p = (Period) deserialize(serializedForm);
        System.out.println(p);
    }

    // Returns the object with the specified serialized form
    private static Object deserialize(byte[] sf) {
        try {
            InputStream is = new ByteArrayInputStream(sf);
            ObjectInputStream ois = new ObjectInputStream(is);
            return ois.readObject();
        } catch (Exception e) {
            throw new IllegalArgumentException(e);
        }
    }
}

   为解决这个问题,要为该类提供一个readObject方法,该方法首先调用defaultReadObject,然后检查反序列化之后的对象的有效性。

// readObject method with validity checking - Page 304
    // This will defend against BogusPeriod attack but not MutablePeriod.
//  private void readObject(ObjectInputStream s)
//          throws IOException, ClassNotFoundException {
//      s.defaultReadObject();
//
//      // Check that our invariants are satisfied
//      if (start.compareTo(end) > 0)
//          throw new InvalidObjectException(start +" after "+ end);
//  }


(3)伪造字节流,创建可变实例
public class MutablePeriod {
    // A period instance
    public final Period period;

    // period's start field, to which we shouldn't have access
    public final Date start;

    // period's end field, to which we shouldn't have access
    public final Date end;
    public MutablePeriod() {
        try {
            ByteArrayOutputStream bos =
                new ByteArrayOutputStream();
            ObjectOutputStream out =
                new ObjectOutputStream(bos);

            // Serialize a valid Period instance
            out.writeObject(new Period(new Date(), new Date()));

            /*
             * Append rogue "previous object refs" for internal
             * Date fields in Period. For details, see "Java 
             * Object Serialization Specification," Section 6.4.
             */
            byte[] ref = { 0x71, 0, 0x7e, 0, 5 }; // Ref #5
            bos.write(ref); // The start field
            ref[4] = 4;     // Ref # 4
            bos.write(ref); // The end field

            // Deserialize Period and "stolen" Date references
            ObjectInputStream in = new ObjectInputStream(
            new ByteArrayInputStream(bos.toByteArray()));
            period = (Period) in.readObject();
            start  = (Date)   in.readObject();
            end    = (Date)   in.readObject();
        } catch (Exception e) {
            throw new AssertionError(e);
        }
    }

    public static void main(String[] args) {
        MutablePeriod mp = new MutablePeriod();
        Period p = mp.period;
        Date pEnd = mp.end;

        // Let's turn back the clock
        pEnd.setYear(78);
        System.out.println(p);

        // Bring back the 60s!
        pEnd.setYear(69);
        System.out.println(p);
    }
}

   提供足够的保护性拷贝

// readObject method with defensive copying and validity checking - Page 306
    // This will defend against BogusPeriod and MutablePeriod attacks.
//  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 our invariants are satisfied
//      if (start.compareTo(end) > 0)
//          throw new InvalidObjectException(start +" after "+ end);
//  }


(4)编写健壮的readObject方法

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

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

C、如果整个对象在被反序列化之后必须进行验证,就应该使用ObjectInputValidation接口

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


4、对于实例控制,枚举类型优先于readResolve

(1)readResolve方法

    对于一个正在被反序列化的对象,如果它的类定义了一个readResolve方法,并且具备正确的声明,那么在反序列化之后,新建对象上的readResolve方法就会被调用,然后该方法的返回的对象引用将取代新建的对象。

(2)对象引用类型的所有实例域必须为transient

    如果依赖readResolve进行实例控制,则对象引用类型的所有实例域必须为transient

// Broken singleton - has nontransient object reference field!
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;
    }
}


(3)使用枚举类型进行实例控制的约束条件
// Enum singleton - the preferred approach - Page 311

import java.util.*;

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



5、考虑用序列化代理代替序列化实例

(1)序列化代理模式
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 - page 312
    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 - Page 313
        private Object readResolve() {
            return new Period(start, end);  // Uses public constructor
        }
    }

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

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

 A、为可序列化的类设计一个私有静态嵌套类

 B、产生一个SerializationProxy,将外围实例转变为它的序列化代理

 C、在SerializationProxy类里头提供一个readResolve方法,返回一个逻辑上相当的外围类的实例


(2)序列化代理的局限

A、不能与可被客户端扩展的类兼容

B、不能与对象图中包含循环的某些类兼容

C、性能开销增大


你可能感兴趣的:(【10】Effective Java - 序列化)