EffectiveJava——序列化

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

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

实现Serializable接口一定要显示的声明UID,否则系统计算的UID可能不一样,在反序列化的时候如果发现UID不一样,就会抛出InvalidClassException

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

实现Serializable接口而付出的第三个代价是:随着版本发行,相关测试负担也增加了
序列化与反序列化

反序列化也是一个隐藏的构造器,它不会调用默认的构造器

如果一个为了继承而设计的类不是可序列化的,那么就不可能编写出可序列化的子类,如果超类没有提供可访问的无参构造方法,子类也不可能做到序列化。

内部类不应该实现Serializable接口(外围实例引用有关),静态内部类却是可以的。

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

如果没有认真考虑默认的序列化形式是否合适,则不要贸然接受。

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

下面对于人名使用默认序列化是合理的。

public class Name implements Serializable {
    protected static final long serialVersionUID = 2123L;

    private  final String lastName;
    private  final String firstName;

    public Name(String lastName, String firstName) {
        this.lastName = lastName;
        this.firstName = firstName;
    }
}

即使你确定了默认序列化形式是合适的,同样还应该提供一个readObject方法以保证约束关系和安全就行。

下面一个例子是另一个极端:

public class StringList implements Serializable {

    protected static final long serialVersionUID = 42L;

    private int size = 0;
    private Entry head = null;

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

从逻辑意义上来讲,这个类表示了一个字符串序列化,但从物理意义上来讲,它把该序列化表示成一个双向链表。如果你介绍了默认序列化,该序列化形式将不遗余力的镜像出链表中的所有项。以及这些项之间的所以双向连接:

如果一个对象的物理表示法不同于他的逻辑内容,使用默认序列化有以下四个却缺点:

  • 它是这个类的导出API永远的束缚在该类的内部表示法上
    如果改变了内部实现,反序列化将不可能成功
  • 它会消耗过多的空间
  • 它会消耗过多的时间
  • 它会引起栈溢出,序列化的过程要对对象图执行一次递归遍历。

    StringList stringList = new StringList();
    for (int i = 0; i < 10000; i++) {
    stringList.add(String.valueOf(i));
    }

对于StringList应该使用自定义的序列化形式:

    private void writeObject(ObjectOutputStream stream) throws IOException {
        System.out.println("writeObject() called with " + "stream = [" + stream + "]");
        stream.defaultWriteObject();
        stream.writeInt(size);
        for (Entry e = head; e != null; e = e.next) {
            stream.writeObject(e.data);
        }
    }

    private void readObject(ObjectInputStream stream) throws IOException, ClassNotFoundException {
        System.out.println("readObject() called with " + "stream = [" + stream + "]");
        stream.defaultReadObject();

        int size = stream.readInt();
        for (int i = 0; i < size; i++) {
            add((String) stream.readObject());
        }
    }

这样可以有效的防止栈溢出,并且对于之后的内部表示法可以灵活的修改。
记住自定义序列化的步骤:

  1. 定义transient
  2. writeObject和readObject首要任务调用defaultWriteObject,

无论是否是默认的序列化,当default方法被调用的时候,对于每一个未被transient的实例域,当调用defaultWriteObject时,都会被序列化

在决定将一个与做成非transient的之前,请一定要确认它的值将是该对象逻辑状态的一部分。

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

不管你是否使用默认序列化,都要为自己编写的每一个可序列化的类声明一个显式的serialVersionUID

使用默认序列化时,请务必考虑类今后的兼容性,使用默认序列化的域,必须永久的保存下去,以确保兼容性。

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

一个被序列化化到文件的对象表示如下:

aced 0005 7372 0014 6d61 696e 2e74 6573
742e 5374 7269 6e67 4c69 7374 0000 0000
0000 002a 0300 0078 7077 0400 0027 1074
0001 3071 007e 0002 7400 0131 7400 0132
7400 0133 7400 0134 7400 0135 7400 0136
7400 0137 7400 0138 7400 0139 7400 0231
3074 0002 3131 7400 0231 3274 0002 3133
7400 0231 3474 0002 3135 7400 0231 3674
0002 3137 7400 0231 3874 0002 3139 7400
0232 3074 0002 3231 7400 0232 3274 0002
3233 7400 0232 3474 0002 3235 7400 0232
3674 0002 3237 7400 0232 3874 0002 3239
......

这个二进制数据是有可能被篡改的,为了保证对象正常的反序列化,需要做一下措施:
当调用defaultReadObject后,检查被反序列化之后对象的有效性,如果被反序列化之后对象无效,抛出invalidObjectException

public final class Period{
   private final Date start;
   private final Date end;
  ...
}

但是为了防止被反序列化之后对象的内部组件被篡改,还需要进步措施:
问题的根源在于:readObject方法没有完成足够的保护性拷贝。当一个对象被序列化的时候,对于客户端不应该拥有对象的引用,如果哪个域包含了这个对象的引用,就必须做保护性拷贝。

private void readObject(ObjectInputStream s)...{
    s.defaultReadObject();
    //保护性拷贝。
    start = new Date(start.getTime());
    end = new Date(end.getTime());
   // 有效性检查
     if(start.compareTo(end) > 0){
        throw new InvalidObjectException(start + " after " + end);
      }
}

总之:每当编写readObject方法的时候,都要这样想,你正在编写一个共有的构造器,无论它传递什么样的字节流,它都必须产生一个有效的实例,不要假设这个字节流一定代表着一个真正被实例化过的实例。

下面步骤有助于编写更加健壮的readObject方法:

  • 私有可变域进行保护性拷贝
  • 在保护性拷贝之后进行约束检查,不符合约定则抛出invalidObjectException
  • 如果对于整个对象图需要在被反序列化之后必须进行验证,就是用ObjectInputValidation接口
  • 无论是间接还是直接,都不要调用类中任何可以被覆盖的方法

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

对于一个单利:

public class Lazy {
    public static final Lazy INSTANCE = new Lazy();
    private Lazy() {
        System.out.print("Lazy init");
    }
}

如果这个类实现了Serializable,他就不再是一个单利。实现了Serializable相当于提供了另一个构造器。

readResolve特性允许你用readObject创建的示例代替另一个实例,它需要如下声明:

    private Object readResolve() {
        return SINGLETON_TEST;
    }

如果依赖readResolve进行实例控制,带有对象引用类型的所有实例域都应该被声明为transient的

readResolve的可访问性很重要,如果类是fainl的,它应该是私有的,如果类是非final的需要认真考虑它的可访问性,对于一个受保护的或者共有的readResolve,如果子类没有覆盖它,那么子类在反序列化的时候返回就是父类的实例对象,这一点需要注意。

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

序列化代理模式的实现很简单:
首先为可序列化的类设计一个私有的静态的嵌套类,精确的表示外围实例的逻辑状态,这个嵌套类被称为序列化代理类。首先它要有一个独立的构造方法,参数类型就是他的外围类。

对于类:

 public final class Period{
       private final Date start;
       private final Date end;
      ...
    }

它的序列化带来应该是这样的:

private static class SeralizationProxy implements Seralizable{
      private final Date start;
      private final Date end;
      SeralizationProxy(Period p){
         this.start = p.start;
         this.end = p.end;
       }
     uid...
}

接下来,将下面writeReplace方法添加到外围类中,通过序列化代理,可以被逐字的复制到任何类中:

private Object writeReplace(){
   return new SeralizationProxy();
}

有了这个方法,序列化系统永远不会产生外围类的序列化实例。但是为了防止反序列化,在Period中定义如下方法:

private void readObject(ObjectInputStream s)throws InvalidObjectException{
    throw new InvalidObjectException("");
}

最后SeralizationProxy提供一个readResolve方法,返回一个逻辑上相当于外围类的示例。

private Object readResolve(){
   return new Period(start,end);
}

序列化代理有两个局限性:不能与可以被客户端扩展的类兼容,不能与对象图中包含循环的某些类兼容。

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

EffectiveJava决对是一本Java的经典书籍,阅读此书懂得了很多!

你可能感兴趣的:(EffectiveJava——序列化)