Effective java笔记(十),序列化

将一个对象编码成字节流称作将该对象「序列化」。相反,从字节流编码中重新构建对象被称作「反序列化」。一旦对象被「序列化」后,它的编码就可以从一台虚拟机传递到另一台虚拟机,或被存储到磁盘上,供以后「反序列化」使用。序列化技术为JavaBean组件结构提供了标准的持久化数据格式。

74、谨慎的实现Serializable接口

一个类实现Serializable接口需要付出的代价:

  • 一旦一个类被发布,就大大降低了「改变这个类的实现」的灵活性。若一个类实现了Serializable接口,它就成了这个类导出API的一部分。
  • 增加了出现Bug和安全漏洞的可能性。序列化机制是一种语言之外的对象创建机制,反序列化是一个「隐藏的构造器」,具备与其他构造器相同的特点。因此,反序列化过程必须要保证所有的约束关系。
  • 随着发行新的版本,相关的测试负担也增加了。

每个可序列化的类都有一个唯一的名为serialVersionUID的标识号与它相关联。若类在私有的静态final的long域中没有显式的指定这个标识号,系统就会自动的为该类产生一个标识号,这时类的兼容性将会遭到破坏,在运行时导致InvalidClassException异常。

为了继承而设计的类应该尽可能少的去实现Serializable接口,用户自定义的接口也应该尽可能少的继承Serializable接口。例外,Throwable、Component和HttpServlet抽象类。

内部类不应该实现Serializable即可。静态成员类可以实现Serializable接口。

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

对于一个对象来说,理想的序列化形式应该只包含该对象所表示的逻辑数据,而逻辑数据与物理表示法(存储结构)应该是独立的。如果一个对象的物理表示法等同于它的逻辑内容,就适用于使用默认的序列化形式。如:

public class Name implements Serializable {
    /**
     * Last name. Must be non-null.
     * @serial 
     */
    private final String lastName;

    /**
     * first name. Must be non-null.
     * @serial 
     */
    private final String firstName;

    private final String middleName;

    ....
}

在这段代码中,Name类的实例域精确的反应了它的逻辑内容,可以使用默认的序列化形式。注意:虽然lastName、firstName和middleName域是私有的,但它们仍然需要有注释文档。因为,这些私有域定义了一个公有的API,即这个类的序列化形式。@serial标签用来告知Javadoc工具,把这些文档信息放在有关序列化形式的特殊文档页中。

当一个对象的物理表示法与它的逻辑内容之间有实质性的不同时,使用默认序列化形式有如下缺点:

  • 它将这个类的导出API永远束缚在了该类的内部表示法上。如,私有内部类变成公有API的一部分。
  • 会消耗过多的空间和时间
  • 会引起栈溢出
  • 其约束关系可能遭到严重破坏,如散列表

如:

//默认序列化形式
public final class StringList implements Serializable {
    private int size = 0;
    private Entry head = null;

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

自定义序列化:


public final class StringList implements Serializable {

    private static final long serialVersionUID = ...;
    private transient int size = 0; //不会被序列化
    private transient Entry head = null;

    private static class Entry {
        String data;
        Entry next;
        Entry previous;
    }
    
    public final void add(String s) { ... }

    /**
     * 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);
        for(Entry e = head; e != null; e = e.next ) {
            s.writeObject(e.data);
        }
    }

    private void readObject(ObjectInputStream s) 
        throws IOException, ClassNotFoundException {
        s.defaultReadObject();
        int num = s.readInt();
        for(int i=0; i < num; i++) {
            add((String)s.readObject());
        }
    }
    .....
}

注意:尽管StringList的所有域都是transient,但writeObject和readObject的首要任务仍是调用defaultXxxObject方法,这样可以极大的增强灵活性。另外尽管writeObject是私有的,仍然需要文档注释。

无论自定义序列化还是默认序列化,对于一个线程安全的对象,必须在序列化方法上强制同步。如:

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

总之,当要将一个类序列化时,应该仔细考虑采用默认序列化还是自定义序列化。选择错误的序列化形式对于一个类的复杂性和性能都会有永久的负面影响。

76、保护性编写readObject方法

readObject方法实际上相当于一个公有的构造器,如同其它构造器一样,readObject方法必须检查其参数的有效性,并且在必要的时候进行保护性拷贝。readObject是一个「用字节流作为唯一参数」的构造器,当面对一个人工仿造的字节流时,readObject产生的对象可能会违反它所属类的约束条件,所以必须在readObject中增加约束性检查,若有效性检查失败,抛出InvalidObjectException异常。如:

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

    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 getStart() {
        return new Date(start.getTime());
    }

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

    //反序列化时增加约束条件
    private void readObject(ObjectInputStream s) throws IOException, ClassNotFoundException {
        s.defaultReadObject();
        if(start.compareTo(end) > 0) {
            throw new InvalidObjectException(start + " after " + end);
        }
    }
}

在这段代码中,尽管readObject中增加了有效性检查,但通过伪造字节流创建可变的Period实例仍是可能的。做法是:字节流以Period实例开头,然后附加上两个额外的引用执行Period实例中两个私有的Date域。攻击者从ObjectInputStream中读取Period实例,然后读取其后的「恶意引用」,通过这个引用攻击者就可以修改Period中私有的Date域。如:

ByteArrayOutputStream bos = new ByteArrayOutputStream();
ObjectOutputStream out = new ObjectOutputStream(bos);
Period period = new Period(new Date(), new Date());
out.writeObject(period);
byte[] ref = {0x71, 0, 0x7e, 0, 5}; //指向period中私有域start的字节
bos.write(ref);
ref[4] = 4; //指向period中私有域end的字节
bos.write(ref);

//反序列化
ObjectInputStream in = new ObjectInputStream(new ByteArrayInputStream(bos.toByteArray()));
Period period1 = (Period)in.readObject();
//ref1指向period1中私有域start指向的对象,可通过这个引用修改不可变对象
Date ref1 = (Date)in.readObject(); 
Date ref2 = (Date)in.readObject();

因此,对于每个可序列化的不可变类,若它包含了私有的可变组件(对象的引用),那么在它的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);
    }
}

注意:final域必须在对象构造时初始化,为了使用readObject方法,必须将start和end域做成非final的。

编写readObject方法的指导原则:

  • 对于对象引用域必须保持为私有的类,要保护性的拷贝这些域中的每个对象。
  • 对于任何约束条件,若检查失败,则抛出一个InvalidObjectException异常。检查应在保护性拷贝之后。
  • 无论直接方式还是间接方式,都不要调用类中任何可被覆盖的方法,否则反序列时可能会失败。

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

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

如上所示,若这个Singleton类的声明上加上「implements Serializable」,它就不再是一个Singleton。无论该类使用的是默认的序列化形式还是自定义的序列化形式。因为任何一个readObject方法,它都会返回一个新建的实例

对于一个正在被反序列化的对象,若它的类定义了一个readResolve方法,那么在反序列化后,新建对象上的readResolve方法就会被调用。然后该方法返回的对象引用将被返回,取代新建的对象,而新建的对象将被垃圾回收。

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

    private Object readResolve() {
        //Return the one true Elvis
        return INSTANCE;
    }
}

该方法忽略了被反序列化的对象,只返回该类初始化时创建的Elvis实例。若依赖readResolve进行实例控制,带有对象引用类型的所有实例域则都必须声明为transient的。,否则能被人工仿造的字节流攻击。静态成员不属于对象,不参与序列化。

通过将一个可序列化的实例受控的类编写成枚举,可以绝对保证除了所声明的常量外,不会有别的实例。如:

public enum Elvis {
    INSTANCE;
    ....
}

另外,readResolve的可访问性很重要。若把readResolve方法放在一个final类上,它就应该是私有的。若readResolve方法是受保护的或共有的,并且子类没有覆盖它,对序列化过的子类实例进行反序列化,就会产生一个超类实例,这可能导致ClassCastException异常。

总之,应该尽可能的使用枚举类型来实施实例控制的约束条件,若做不到,就必须提供一个readResolve方法,并将引用类型的域声明为transient的。

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

序列化代理模式能够极大的减少实现Serializable接口所带来的风险。

实现序列化代理模式的步骤:

  • 首先为可序列化的类设计一个私有的静态嵌套类,精确的表示外围类实例的逻辑状态。它有一个单独的构造器,其参数类型为外围类。外围类及其序列化代理都必须实现Serializable接口。
  • 将writeReplace方法添加到外围类中。
  • 在SerializableProxy类中提供readResolve方法,它返回逻辑上相等的外围类的实例。

如:

//外围类不需要serialVersionUID
public final class Period implements Serializable {
    private final Date start;
    private final Date end;

    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 getStart() {
        return new Date(start.getTime());
    }

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

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

    //防止被攻击者使用
    private void readObject(ObjectInputStream stream) 
        throws InvalidObjectException{
        throw new InvalidObjectException("Proxy required");
    }

    private static class SerializationProxy implements Serializable {
        private static final long serialVersionUID = ...;
        private final Date start;
        private final Date end;

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

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

正如保护性拷贝一样,序列化代理可以阻止伪字节流的攻击及内部域的盗用攻击。与使用保护性拷贝不同,使用序列化代理允许Period的域为final的,这可以保证Period类真正不可变。序列化代理模式更容易实现,它不必考虑哪些域会被序列化攻击,也不必显示的执行有效性检查。

序列化代理的局限性:不能与可以被客户端扩展的类兼容,也不能与对象图中包含循环的类兼容,比保护性拷贝性能低。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 159,716评论 4 364
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,558评论 1 294
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 109,431评论 0 244
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,127评论 0 209
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,511评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,692评论 1 222
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,915评论 2 313
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,664评论 0 202
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,412评论 1 246
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,616评论 2 245
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,105评论 1 260
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,424评论 2 254
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,098评论 3 238
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,096评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,869评论 0 197
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,748评论 2 276
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,641评论 2 271

推荐阅读更多精彩内容

  • JAVA序列化机制的深入研究 对象序列化的最主要的用处就是在传递,和保存对象(object)的时候,保证对象的完整...
    时待吾阅读 10,739评论 0 24
  • 对象的创建与销毁 Item 1: 使用static工厂方法,而不是构造函数创建对象:仅仅是创建对象的方法,并非Fa...
    孙小磊阅读 1,878评论 0 3
  • 官方文档理解 要使类的成员变量可以序列化和反序列化,必须实现Serializable接口。任何可序列化类的子类都是...
    狮_子歌歌阅读 2,337评论 1 3
  • 正如前文《Java序列化心得(一):序列化设计和默认序列化格式的问题》中所提到的,默认序列化方法存在各种各样的问题...
    登高且赋阅读 8,126评论 0 19
  • 本章关注对象序列化API,它提供了一个框架,用来将对象编码成字节流,并从字节流编码中重新构建对象。 相反的处理过程...
    Timorous阅读 234评论 0 1