java序列化总结

本文将首先介绍序列化的用途,然后介绍序列化的基本用法,继而提出在序列化使用中需要注意的一些规范;如果有进一步兴趣,可以继续查看序列化的原理,加深理解。


1.序列化用途

所谓的序列化,就是把对象转成对应的字节数组;反序列化,即把字节数组转成其关联的对象。
这些字节序列中保存的信息其实是这个对象的状态信息,即它的成员变量。
大家知道,计算机存储或者通讯,主要是通过字节流来进行。因此,通过序列化,我们可以把对象进行本地持久化,或者进行网络传输,例如rmi、rpc机制中对象的传输。


2.如何序列化

我们首先构建一个本次需要进行序列化的类

public class PersonUsingSerializable implements Serializable {
    private static final long serialVersionUID =1L;
    private String name;
    private int age;
    private LocalDatebirthday;
    /**
    * 字段不需要持久化
    */
    transient private int seq;

    public String get Name() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public int getAge() {
        return age;
    }

    public void setAge(int age) {
        this.age = age;
    }

    public LocalDate getBirthday() {
        return birthday;
    }

    public void setBirthday(LocalDate birthday) {
        this.birthday = birthday;
    }

    @Override
    public String toString() {
        return String.format("Person, name:[%s], age:[%d], birthday:[%s], seq:[%d]",this.getName(), this.getAge(), this.getBirthday(), this.getSeq());
    }

    public int getSeq() {
        return seq;
    }

    public void setSeq(int seq) {
        this.seq = seq;
    }
}

2.1 基本操作:实现java.io.Serializable接口

对于比较简单的类(类似一个单纯且无复杂数据结构的类),像上述PersonUsingSerializable类,我们仅需实现一个接口即可。然后利用java.io.ObjectOutputStream/java.io.ObjectInputStream,完成相关的字节流输出/输入。

public class SerializableDemo {
    public static void main(String[] args) throws Exception{
        PersonUsingSerializable person =new PersonUsingSerializable();
        person.setName("peter");
        person.setAge(18);
        person.setBirthday(LocalDate.of(1989, 1, 1));
        person.setSeq(10);

        final String filePath ="E://tmp/person.txt";

        System.out.println("persisting person...");
        ObjectOutputStream oos =new ObjectOutputStream(new FileOutputStream(filePath));
        oos.writeObject(person);

        System.out.println("restore person...");
        ObjectInputStream ois =new ObjectInputStream(new FileInputStream(filePath));
        Object restorePerson = ois.readObject();
        System.out.println("profile of this object:" + restorePerson);
    }
}

可以看到输出结果,对象能正常序列化及反序列化,注意其中seq字段并未被持久化:

persisting person...
restore person...
profile of this object:Person, name:[peter], age:[18], birthday:[1989-01-01], seq:[0]

默认序列化机制中,只会序列化非static和非transient字段。

2.2 定制操作

如果不满足于默认的java序列化机制,jdk提供了如下接口:

  • private void writeObject(java.io.ObjectOutputStream out) throws IOException;

针对特定的类字段的状态保存,可以使用该方法。其它默认字段的保存可以通过ObjectOutputStream#defaultWriteObject()方法触发(值得一提,对于实现了这个方法的类,可以不关心它的子类或超类的相关字段的保存)。特定类的字段保存,可以通过调用ObjectOutputStream的writeObject方法或对应的一些原始类型的写入方法。

  • private void readObject(java.io.ObjectInputStream in) throws IOException,ClassNotFoundException;

这个方法和writeObject相对应。针对子类或超类、其它一些默认字段,通过InpupStream的defaultReadObject方法,触发默认的反序列化机制。需要注意一点,需要单独操作的读字段的顺序要和对象的写字段顺序一致。

  • private void readObjectNoData() throws ObjectStreamException;

如果反序列化类和序列化类,在继承体系上不兼容(比如:反序列化的类继承的类和序列化的类继承的类的版本不一致或者序列化流被破坏,数据有损),这个时候该方法就可以排上用场。

  • ANY-ACCESS-MODIFIER Object writeReplace() throws ObjectStreamException;

当需要替换写入对象时,可以使用该方法。

  • ANY-ACCESS-MODIFIER Object readResolve() throws ObjectStreamException;

如果需要替换读入的反序列化对象时,可以使用该方法。该方法常用在单例模式中。

  • 相关代码示例:
  1. writeObject()和readObject()
public class PersonUsingSerializable implements Serializable {
    // ......
    private void writeObject(ObjectOutputStream out)throws IOException {
        out.defaultWriteObject();
        out.writeObject(this.getName() +" from serialize");
    }

    private void readObject(ObjectInputStream in)throws IOException, ClassNotFoundException {
        in.defaultReadObject();
        this.name = (String)in.readObject();
        this.seq =10;
    }
}

通过输出结果,可以看出,name的输出被替换了,可以体现在反序列化中;而之前因为transient的原因未序列化的seq字段,因为readObject()中的相关操作,也获得了初始值。

persisting person...
restore person...
profile of this object:Person, name:[peter from serialize], age:[18], birthday:[1989-01-01], seq:[10]
  1. writeReplace()和readResolve()
    感觉应用场景不多。1)在单例模式中,为了序列化和反序列化的对象仍是唯一的单例对象。可以在方法中返回这个单例对象,以免单例模式失败;2)还有一种就是effective java中提倡的:考虑使用序列化代理代替序列化实例。

2.3 彻底掌控序列化:实现java.io.Externalizable

通过实现该接口,对应的类可以完全控制序列化流的格式和内容。实际上,Externalizable接口实现了Serializable接口,java序列化机制会先根据serializable接口,判断对象是否可以序列化;进一步,根据是否有实现了Externalizable接口,判断序列化方式。

示例如下:

public class PersonUsingExternalizable implements Externalizable {
    private static final long serialVersionUID =1L;
    private Stringname;
    private int age;
    private LocalDatebirthday;

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public int getAge() {
        return age;
    }

    public void setAge(int age) {
        this.age = age;
    }

    public LocalDate getBirthday() {
        return birthday;
    }

    public void setBirthday(LocalDate birthday) {
        this.birthday = birthday;
    }

    @Override
    public String toString() {
        return String.format("Person, name:[%s], age:[%d], birthday:[%s]", this.getName(), this.getAge(), this.getBirthday());
    }

    @Override
    public void writeExternal(ObjectOutput out)throws IOException {
        out.writeObject(this.name);
        out.writeInt(this.age);
        out.writeObject(this.birthday);
    }

    @Override
    public void readExternal(ObjectInput in)throws IOException, ClassNotFoundException {
        this.name = (String)in.readObject();
        this.age = in.readInt();
        this.birthday = (LocalDate)in.readObject();
    }
}

3.序列化相关规范或注意事项

可以参考Effective Java中关于序列化操作(第11章)的相关注意事项,这里简单总结如下:

  • 谨慎地实现Serializable接口

在发布一个接口的同时,如果发布的类支持序列化,那么这个类的字节编码也就成为发布的接口的一部分。特别地,如果使用默认的序列化形式,这个类中的私有和包级私有的实例域都将变成导出的API的一部分,这不符合“最低限度地访问域”的实践准则。

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

如果一个对象的物理表示法等同于它的逻辑内容,可能就适合于使用默认的序列化形式。否则,“即使你确定了默认的序列化形式是合适的,通常还必须提供一个readObject方法以保证约束关系和安全性。”

  • 保护性地编写readObject方法

readObject方法实际上相当于另一个共有的构造器,如同其它的构造器一样,它也要求同样的所有注意事项。构造器必须检查其参数的有效性,并且在必要的时候对参数进行保护性拷贝。

  • 考虑使用序列化代理代替序列化实例

4.序列化原理

接下来将介绍如下代码的实现机制

// 1.序列化,简单介绍对象到输出流的基本过程
ObjectOutputStream oos =new ObjectOutputStream(new FileOutputStream(filePath));
oos.writeObject(person);

// 2.反序列化,请参考序列化,不赘述
ObjectInputStream ois =new ObjectInputStream(new FileInputStream(filePath));
Object restorePerson = ois.readObject();

4.1 序列化

分两步。
(1) new ObjectOutputStream():会首先写入序列化的魔法数和版本,以及相关配置的初始化

public ObjectOutputStream(OutputStream out)throws IOException {
    verifySubclass();
    /**
     * 内部是使用块输出流
     **/
    bout =new BlockDataOutputStream(out);
    enableOverride =false;
    // ......
    // 设置文件头
    writeStreamHeader();
    bout.setBlockDataMode(true);
}

(2) ObjectOutputStream#writeObject()方法核心流程

首先我们需要知道,在该核心方法中,需要关注的三个类:

  • java.io.ObjectOutputStream.BlockDataOutputStream

即,ObjectOutputStream方法内部输出流的操作对象

  • java.io.ObjectStreamClass

Serialization's descriptor for classes. It contains the name and serialVersionUID of the class.

  • java.io.ObjectStreamField

A description of a Serializable field from a Serializable class.


  • 接下来,描述下整体关键流程:

a.根据传入的对象的所属类型,获取类序列化描述符ObjectStreamClass(java.io.ObjectStreamClass#lookup);

b.判断序列化对象的类型:String\Array\Enum\Serializable,如果实现了Serializable接口,则走步骤c后的逻辑;

c.根据Externalizable和Serializable接口,走不同的序列化逻辑;

d.如果是externalizable接口,不使用ObjectStreamClass对象,调用传入对象的writeExternal方法写入数据,结束;

e.如果是Serializable接口,根据类是否有writeObject()方法,判断对应逻辑。

f.对于默认的Serializable机制,会通过ObjectStreamClass获取序列化对象的需要序列化的字段,然后针对需要序列化的字段,逐个递归执行序列化操作(步骤b)

附上截取的代码详解:

  • 核心流程(按代码逻辑放置)
// 该方法是对象序列化的入口方法,是整个对象序列化的入口;另外,对象内部的字段也会递归调用该方法进行各自的序列化
private void writeObject0(Object obj, boolean unshared) throws IOException
{
    //......
    ObjectStreamClass desc;
    for (;;) {
        Class repCl;
        // 获取序列化对象的描述符
        desc = ObjectStreamClass.lookup(cl, true);
        if (!desc.hasWriteReplaceMethod() ||
            (obj = desc.invokeWriteReplace(obj)) ==null ||
            (repCl = obj.getClass()) == cl)
        {
            break;
        }
        cl = repCl;
    }
    //......

    // 根据对象类型,判断如何执行输出流,可以看出,如果普通对象没有实现Serializable接口,是没办法进入序列化流程的
    if (obj instanceof String) {
        writeString((String) obj, unshared);
    }else if (cl.isArray()) {
        writeArray(obj, desc, unshared);
    }else if (ob jinstanceof Enum) {
        writeEnum((Enum) obj, desc, unshared);
    }else if (obj instanceof Serializable) {
        writeOrdinaryObject(obj, desc, unshared);
    }else {
        if (extendedDebugInfo) {
        throw new NotSerializableException(cl.getName() +"\n" +debugInfoStack.toString());
        } else {
        throw new NotSerializableException(cl.getName());
    }
}
  • java.io.ObjectStreamClass#lookup方法

该方法内部会查找序列化类是否有对应的描述符,如果没有,会内部new一个ObjectStreamClass,并且进行缓存。需要注意,针对多线程竞争的境况,这里面采用了自旋锁的方式,首先创建一个EntryFuture;成功创建EntryFuture的线程可以进行描述符的创建,其它线程则进入wait操作。

static ObjectStreamClasslookup(Class cl, boolean all) {
    if (!(all || Serializable.class.isAssignableFrom(cl))) {
        return null;
    }

    //通过自旋方式,获得有效的EntryFuture
    //-----------------------------------------------------
    processQueue(Caches.localDescsQueue, Caches.localDescs);
    WeakClassKey key =new WeakClassKey(cl, Caches.localDescsQueue);
    Reference ref = Caches.localDescs.get(key);
    Object entry =null;
    if (ref !=null) {
        entry = ref.get();
    }
    EntryFuture future =null;
    if (entry ==null) {
        EntryFuture newEntry =new EntryFuture();
        Reference newRef =new SoftReference<>(newEntry);
        do {
            if (ref !=null) {
                // 因为该ref不含有有效的Entry,估删除对应的Ref
                Caches.localDescs.remove(key, ref);
            }
            // 尝试放入自己创建的有效的Ref引用;这时候,利用ConcurrentHashMap,保证只有一个插入成功
            ref = Caches.localDescs.putIfAbsent(key, newRef);
            // 判断是否能从插入返回的引用中获取有效的entry,如果没有,会继续本轮循环
            if (ref !=null) {
                entry = ref.get();
            }
        }while (ref !=null && entry ==null);
        if (entry ==null) {
            future = newEntry;
        }
    }
    
    if (entry instanceof ObjectStreamClass) {// check common case first
        return (ObjectStreamClass) entry;
    }
     // 判断EntryFuture的创建线程,如果是其它线程创建,则阻塞等待Future返回描述符即可;否则,置null,以走后续创建逻辑
    // ---------------------------------------------
    if (entry instanceof EntryFuture) {
        future = (EntryFuture) entry;
        if (future.getOwner() == Thread.currentThread()) {
            entry =null;
        } else {
            entry = future.get();
        }
    }

    // 当前线程赢得创建类描述符的权利,开始创建类描述符了
    //------------------------------------------------------------
    if (entry ==null) {
        try {
        // 调用构造方法进行初始化
            entry =new ObjectStreamClass(cl);
        } catch (Throwable th) {
            entry = th;
        }
    if (future.set(entry)) {
        Caches.localDescs.put(key, new SoftReference(entry));
        } else {
    // nested lookup call already set future
            entry = future.get();
        }
    }

    if (entry instanceof ObjectStreamClass) {
        return (ObjectStreamClass) entry;
    } else if (entry instanceof RuntimeException) {
        throw (RuntimeException) entry;
    } else if (entry instanceof Error) {
        throw (Error) entry;
    } else {
        throw new InternalError("unexpected entry: " + entry);
    }
}
  • ObjectStreamClass的构造方法会通过反射,获取序列化类的各种描述信息,包括是否实现serializable接口、是否实现externalizable接口,是否是枚举或代理类,解析可序列化的字段,解析获取私有的writeObject、readObject等方法。
private ObjectStreamClass(final Class cl) {
    //......
    isProxy = Proxy.isProxyClass(cl);
    isEnum = Enum.class.isAssignableFrom(cl);
    serializable = Serializable.class.isAssignableFrom(cl);
    externalizable = Externalizable.class.isAssignableFrom(cl);
    //......
    fields =getSerialFields(cl);
    //......
    if (externalizable) {
        cons =getExternalizableConstructor(cl);
    } else {
        cons =getSerializableConstructor(cl);
        writeObjectMethod =getPrivateMethod(cl, "writeObject",
        new Class[] { ObjectOutputStream.class },
        Void.TYPE);
        readObjectMethod =getPrivateMethod(cl, "readObject",
        new Class[] { ObjectInputStream.class },
        Void.TYPE);
        readObjectNoDataMethod =getPrivateMethod(cl, "readObjectNoData", null, Void.TYPE);
        hasWriteObjectData = (writeObjectMethod !=null);
    }
    writeReplaceMethod =getInheritableMethod(cl, "writeReplace", null, Object.class);
    readResolveMethod =getInheritableMethod(cl, "readResolve", null, Object.class);
    // ......
}
  • 一切就绪,开始序列化了,看看writeOrdinaryObject方法
private void writeOrdinaryObject(Object obj,
                                ObjectStreamClass desc,
                                boolean unshared)
throws IOException
{
    if (extendedDebugInfo) {
        debugInfoStack.push((depth ==1 ?"root " :"") +"object (class \"" +obj.getClass().getName() +"\", " + obj.toString() +")");
    }

    try {
        desc.checkSerialize();
        bout.writeByte(TC_OBJECT);
        writeClassDesc(desc, false);
        handles.assign(unshared ?null : obj);
        if (desc.isExternalizable() && !desc.isProxy()) {
            // 执行实现了Externalizable的类的序列化
            // 该方法内部实际上就是直接调用方法的writeExternal方法,然后结束序列化操作
            writeExternalData((Externalizable) obj);
        } else {
            // 执行实现了Serializable的类的序列化
            writeSerialData(obj, desc);
        }
    } finally {
        if (extendedDebugInfo) {
            debugInfoStack.pop();
        }
    }
}
  • 对于使用默认序列化的类,writeSerialData方法内部,会调用defaultWriteFields方法,这个方法会循环遍历描述符中的每一个需要序列化的字段,然后调用writeObject0方法,走对象序列化流程。
private void defaultWriteFields(Object obj, ObjectStreamClass desc)
throws IOException
{
    Class cl = desc.forClass();
    if (cl !=null && obj !=null && !cl.isInstance(obj)) {
        throw new ClassCastException();
    }

    desc.checkDefaultSerialize();
    int primDataSize = desc.getPrimDataSize();
    if (primVals ==null ||primVals.length < primDataSize) {
        primVals =new byte[primDataSize];
    }
    desc.getPrimFieldValues(obj, primVals);
    bout.write(primVals, 0, primDataSize, false);
    ObjectStreamField[] fields = desc.getFields(false);
    Object[] objVals =new Object[desc.getNumObjFields()];
    int numPrimFields = fields.length - objVals.length;
    desc.getObjFieldValues(obj, objVals);

    for (int i =0; i < objVals.length; i++) {
        if (extendedDebugInfo) {
            debugInfoStack.push(
            "field (class \"" + desc.getName() +"\", name: \"" + fields[numPrimFields + i].getName() +"\", type: \"" +fields[numPrimFields + i].getType() +"\")");
        }

        try {
            // 又走到序列化对象的方法啦
            writeObject0(objVals[i],fields[numPrimFields + i].isUnshared());
        } finally {
            if (extendedDebugInfo) {
                debugInfoStack.pop();
            }
        }
    }
}

4.2 反序列化

可以参照序列化方法自个分析下。

5.其它序列化框架

除了java原生自带的序列化框架,出于压缩速度、压缩比率的角度,一般可以使用其它的序列化框架。这里搜集了一些其它序列化框架进行比较。

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

推荐阅读更多精彩内容

  • JAVA序列化机制的深入研究 对象序列化的最主要的用处就是在传递,和保存对象(object)的时候,保证对象的完整...
    时待吾阅读 10,836评论 0 24
  • 在Java中,我们可以通过多种方式来创建对象,并且只要对象没有被回收我们都可以复用该对象。但是,我们创建出来的这些...
    懒癌正患者阅读 1,521评论 0 12
  • Java序列化总结 序列化就是指将Java对象转换成一系列的字节,反序列化即使将一系列的字节恢复成Java对象。序...
    ObadiObada阅读 503评论 0 2
  • 官方文档理解 要使类的成员变量可以序列化和反序列化,必须实现Serializable接口。任何可序列化类的子类都是...
    狮_子歌歌阅读 2,386评论 1 3
  • 片段一: 众人先是一怔,后来一想,上上下下都一齐哈哈大笑起来。湘云撑不住,一口茶都喷出来。黛玉笑岔了气,扶着桌子...
    小姜老师0301阅读 393评论 1 7