将一个对象编码成一个字节流,称为序列化(serializing);相反的处理过程,称之为反序列化(deserializing)
最大的代价就是一旦一个类被发布,就大大降低了“改变这个类的实现”的灵活性。因为一旦你接受了默认的序列化形式,并且以后又要改变这个类的内部表示法,结果可能导致序列化形式的不兼容。
客户端程序企图用这个类的旧版本来序列化一个类,然后用新版本进行反序列化,结果将导致程序失败。
序列化版本(serial version UID),每个序列化的类都有一个唯一标识号与它相关联,如果没有一个静态私有final的long域显示指定该标识号,系统会自动地根据这个类来调用一个复杂的运算过程,从而在运行时产生标识号。
序列化是一种语言之外的对象创建机制,反序列化是一个隐藏的构造器,它容易使对象的约束关系遭到破坏,以及遭到非法访问。
需要确保序列化反序列化过程的成功,也要确保结果产生的对象真正是原始对象的复制品。
根据经验,Date类和BigInteger这样的值类应该实现序列化接口(大多数集合类也应该如此)。
对于为继承而设计的不可序列化的类,应该考虑提供一个无参构造器。
内部类使用编译器产生的合成域来保存指向外围实例的引用,以及保存来自外围作用域的局部变量的值。内部类的默认序列化形式是定义不清楚的,而静态成员类可以。
如果一个对象的物理表示法等同于它的逻辑内容,可能就适合使用默认的序列化形式。
通常还必须提供一个readObject方法以保证约束关系和安全性。
此时使用默认序列化有如下缺点:
A、导出的API永远束缚在该类的内部表示法上
B、消耗过多空间
C、消耗过多时间
D、会引起栈溢出
// 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 }
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; } }
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); // }
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); // }
A、对于对象引用域必须保持为私有类,要保护性地拷贝这些域中的每个对象,不可变类的可变组件就属于这一类别
B、对于任何约束条件,如果检查失败,则抛出一个InvalidObjectException,这些检查动作应该跟在所有的保护性拷贝之后
C、如果整个对象在被反序列化之后必须进行验证,就应该使用ObjectInputValidation接口
D、无论是直接方式,还是间接方式,都不要调用类中任何可被覆盖的方法。
对于一个正在被反序列化的对象,如果它的类定义了一个readResolve方法,并且具备正确的声明,那么在反序列化之后,新建对象上的readResolve方法就会被调用,然后该方法的返回的对象引用将取代新建的对象。
如果依赖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; } }
// 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)); } }
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方法,返回一个逻辑上相当的外围类的实例
A、不能与可被客户端扩展的类兼容
B、不能与对象图中包含循环的某些类兼容
C、性能开销增大