序列化详解

1.序列化

序列化是指将对象按照某种协议格式转化为二进制字节序列;对应的,反序列化将二进制字节序列恢复为对象。
序列化常用于两种情况:

  1. 数据持久化。利用序列化将对象持久化到外部存储中,然后在合适的时机,通过反序列化恢复为对象。目前此种用法较少,使用数据库持久化是更好的选择。
  2. 网络传输对象。将对象序列化为二进制字节数据,通过网络传输到远程机器,然后通过反序列化恢复为对象。这是序列化使用的主要场景,远程方法调用RMI和远程过程调用RPC都依赖于序列化。

JAVA序列化的设计初衷是为了保存对象的状态从而保存进程状态,以便在合适的时机恢复进程,但目前已基本脱离这种用法。JAVA序列化的另一大用法是远程过程调用RMI,由于性能不高(序列化后所占存储空间过大且序列化耗时较高)的原因也很少用于实际生产环境中。生产环境中,建议使用性能更高的序列化方案,比如Google的ProtoBuf、FaceBook的Thrift,以及用于HTTP的JSONXML等等。尽管原生JAVA序列化方案已较少使用,但理解其原理也大有裨益。

2. 序列化的基本用法

在JAVA中,一个对象要实现序列化是极其简单的:实现Serializable接口即可。

class User implements Serializable {
    private String name;
    private String password;
    
    public User(String name, String password) {
        this.name = name;
        this.password = password;
        System.out.println("constructor");
    }
}

然后再配合ObjectOutputStreamObjectInputStream即可实现对象的序列化和反序列化,一个简单的使用示例如下:

public static void main(String[] args) throws Exception {
    User user = new User("root", "root");

    ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
    ObjectOutputStream out = new ObjectOutputStream(outputStream);
    out.writeObject(user);
    out.close();

    ObjectInputStream in = new ObjectInputStream(
            new ByteArrayInputStream(outputStream.toByteArray()));
    User user1 = (User) in.readObject();
    System.out.println(user1);
}

在示例中,实现了将User对象序列化到ByteArrayOutputStream中的字节数组中,然后使用反序列化将字节数组恢复为一个新的对象。一般的,也可以将对象序列化到FileOutputStream中实现持久化。
再次回顾示例代码,需要注意以下几点:

2.1 Serializable是一个标记接口

Serializable只是一个标记接口,其中没有任何方法。但如果一个待序列化对象不实现该接口,将会抛出NotSerializableException异常。其原理,可参考JDK的ObjectOutputStream源码

    if (obj instanceof String) {
        writeString((String) obj, unshared);
    } else if (cl.isArray()) {
        writeArray(obj, desc, unshared);
    } else if (obj instanceof Enum) {
        writeEnum((Enum) obj, desc, unshared);
    } else if (obj instanceof Serializable) {
        writeOrdinaryObject(obj, desc, unshared);
    } else {
        throw new NotSerializableException(cl.getName());
    }

由此,可知除了实现Serializable接口的对象外,StringArrayEnum也都可以序列化。另外,代码中没有列出的JAVA基本数据类型:intchardouble等也默认可序列化。

2.2 可使用serialVersionUID进行版本控制

如果跨进程使用序列化机制,一个基本要求是:两个进程含有相同的对象class文件,相同指对象的功能和类路径都必须一致。除此之外,两个类的序列化ID也必须一致。JAVA序列化机制使用一个如下的变量serialVersionUID表示序列化ID:

    ANY-ACCESS-MODIFIER static final long serialVersionUID = 42L;

如果两个进程中该值不一致,反序列化时会抛出InvalidClassException异常,从而提醒用户该对象的类可能已被修改了某些字段,需要更新class文件。
注意最开始的示例中没有指定这个变量,此时,编译器会根据类名、接口以及其中所含字段自动生成一个默认值。这个默认值随编译器的实现细节不同而不同,所以强烈建议显式指明该变量值。其中访问权限修饰符建议使用private,一个可序列化对象完整的示例如下:

class User implements Serializable {
    private static final long serialVersionUID = 1L;

    private String name;
    private String password;
}

显式声明该变量值的另一个好处是:可以利用序列化的兼容机制,即:

  1. 新增一个字段。
  2. 改变字段的访问权限修饰符。publicprotectedpackageprivate不会影响序列化。
  3. 将一个字段从static修改为非static或者transient修改为非transient。

这几种情况下,反序列化的class不必和序列化时的class代码完全一致。但如果使用默认serialVersionUID则会生成不一致的值,从而抛出InvalidClassException异常。

2.3 子类的序列化

当一个父类实现序列化,子类自动实现序列化,不需要显式实现Serializable接口。当一个子类实现序列化,而父类没有实现序列化,情况又会怎么样呢?

class User {
    private String name;
    private String password;
    
    // 构造方法省略
}

class Vip extends User implements Serializable{
    private int money;

    public Vip(String name, String password, int money) {
        super(name, password);
        this.money = money;
    }
}

序列化正常,但反序列化时抛出如下异常:

Exception in thread "main" java.io.InvalidClassException: serial.Vip; no valid constructor

为了实现反序列化,有两种方法:

  1. 父类也实现Serializable接口,此时完美符合序列化机制要求。
  2. 父类不实现Serializable接口,但提供一个无参构造方法。此时,子类中的字段可以恢复为序列化前的值,但父类中的字段为初始化默认值。

方法2的示例输出如下:

    Vip{money=5,name=null,password=null}

2.4 静态变量的序列化

JAVA序列化保存的是对象的状态,而静态变量不是对象的状态而是类的状态,所以序列化不保存静态变量

2.5 反序列化不会调用构造方法

JAVA在反序列化时不会调用构造方法。当类有继承关系时,情况又稍有不同。正如2.3的示例,如果子类实现Serializable接口而父类并未实现,但父类有无参构造方法时,尽管子类的构造方法不会被调用,但父类的无参构造方法将会被调用。给UserVIP提供如下的构造方法:

    public User() {
        System.out.println("user no-arg constructor");
    }
    
    public Vip(String name, String password, int money) {
        super(name, password);
        this.money = money;
        System.out.println("vip constructor");
    }

则2.3示例反序列化的输出为:

    user no-arg constructor
    Vip{money=5,name=null,password=null}

可见,子类的构造方法不会被调用,但父类的无参构造方法被调用。

3.自定义序列化

3.1 transient

特殊情况下,某些字段并不希望被序列化,比如说User类中的密码password字段,并不希望被序列化到文件中。也许你会联想到前述子类序列化的示例,父类的字段不会被序列化,可以实现该需求。但这不是好的方式,JAVA提供了一种更好的方法:使用transient关键字。示例如下:

class User implements Serializable{
    private static final long serialVersionUID = 42L;

    private String name;
    private transient String password;
    
    // 构造方法和getter setter省略
}

在不希望被序列化的字段前添加transient关键字,序列化时将忽略该字段。示例的输出如下:

    User{name='root', password='null'}

3.2 writeObject和readObject

在上述的密码password字段的处理中,加密该字段然后序列化到文件是一种更好的方法。可通过在待序列化类添加如下的方法实现:

class User implements Serializable {
    private static final long serialVersionUID = 42L;

    private String name;
    private transient String password;
    
    private void writeObject(ObjectOutputStream out) throws IOException {
        out.defaultWriteObject();
        out.writeUTF(encrypt(password));
    }
    
    private void readObject(ObjectInputStream in)
            throws IOException, ClassNotFoundException {
        in.defaultReadObject();
        String encryptStr = in.readUTF();
        System.out.println("encrypt=" + encryptStr);
        this.password = decrypt(encryptStr);
    }
}

注意其中的writeObject()readObject()方法,可用这两个方法对序列化和反序列化进行特殊处理。本例中,序列化时对密码加密,反序列化时进行解密。本例的输出如下:

    encrypt=63A9F0EA7BB98050796B649E85481845
    User{name='root', password='root'}

如果要实现自己的特殊处理,注意这两个方法的方法签名必须和示例一致,其中访问权限修饰符注意为private,JAVA是通过反射调用的这两个方法。此外,defaultWriteObject()defaultReadObject()是调用JAVA序列化的默认实现。

3.3 Externalizable

第三种自定义序列化过程的方法是:实现Externalizable接口,该接口是Serializable的子接口,代码如下:

public interface Externalizable extends java.io.Serializable {
    // 序列化
    void writeExternal(ObjectOutput out) throws IOException;
    // 反序列化
    void readExternal(ObjectInput in) throws IOException, ClassNotFoundException;
}

如果序列化时实现该接口,则需要自主控制所有序列化细节。也就是说,该接口是一个正常的接口,使用时遵循JAVA语法即可。作为标记接口的Serializable反序列化时不会调用构造方法,而实现该接口反序列时则需要调用类的无参构造方法,然后调用readExternal()方法。使用该接口序列化的示例如下:

class User implements Externalizable {
    private static final long serialVersionUID = 42L;

    private String name;
    private transient String password;

    public User() {}
    
    @Override
    public void writeExternal(ObjectOutput out) throws IOException {
        out.writeUTF(name);
        out.writeUTF(encrypt(password));
    }
    
    @Override
    public void readExternal(ObjectInput in) throws IOException, ClassNotFoundException {
        this.name = in.readUTF();
        String encryptStr = in.readUTF();
        System.out.println(encryptStr);
        this.password = decrypt(encryptStr);
    }
}

最后附上JAVA序列化相关的资料:

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

推荐阅读更多精彩内容

  • JAVA序列化机制的深入研究 对象序列化的最主要的用处就是在传递,和保存对象(object)的时候,保证对象的完整...
    时待吾阅读 10,837评论 0 24
  • Spring Cloud为开发人员提供了快速构建分布式系统中一些常见模式的工具(例如配置管理,服务发现,断路器,智...
    卡卡罗2017阅读 134,599评论 18 139
  • 多视图应用 iOS大多数应用程序都是采用多视图设计。 控制器类型的4种多视图程序:1、自定义视图控制器;(UIVi...
    goyohol阅读 452评论 0 0
  • 01 什么?一天时间环游世界!别逗我。 真不是逗你,周末,我用了整整一天时间,沉浸在毕淑敏的《世界如锦心如梭》中,...
    妮妮小屋阅读 1,122评论 4 23
  • 早晨,我从梦中醒来 我是被鸟儿的鸣叫声幻影的 胖公鸡似乎是睡过了头 迟迟不起 奶奶一...
    孙浒胡阅读 136评论 0 1