likes
comments
collection
share

Java原生序列化机制,serialVersionUID详解

作者站长头像
站长
· 阅读数 23

序列化

什么是序列化

将一个对象转化为字节流,从而能够保存到磁盘,进行网络传输。

Java原生序列化

Java原生序列化要求被序列化的类必须实现如下两个接口之一。

实现Serializable

没有任何实现,只是标志该类的对象是可序列化的。

还需要加上serialVersionUID,当然不加不会报错,至于为什么留到后文分析

实现Externalizable

Externalizable继承了Serializable接口,还定义了两个抽象方法:writeExternal()和readExternal()。

Externalizable强调必须重写writeExternal和readExternal方法,即必须自定义序列化方法,Serializable当然也可以自定义,但不是必须的。并且Externalizable要求类必须提供一个public的无参的构造方法。

Java原生序列化的使用

序列化其实就两个步骤:

  1. 创建一个对象输入流ObjectOutputStream
  2. 调用了ObjectOutputStream.writeObject方法

反序列化也是类似

// 这里以 序列化实现 深拷贝为例
public static <T extends Serializable> T deepCopy(T object) {
    try {
        // 开始序列化
        ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
        ObjectOutputStream objectOutputStream = new                                                     ObjectOutputStream(byteArrayOutputStream);
        // writeObject 方法
        objectOutputStream.writeObject(object);
        // flush 方法
        objectOutputStream.flush();
        // ------------------反序列化--------------------
        ByteArrayInputStream byteArrayInputStream = new                                                 ByteArrayInputStream(byteArrayOutputStream.toByteArray());
        ObjectInputStream objectInputStream = new                                                       ObjectInputStream(byteArrayInputStream);
        // readObject 方法
        return (T) objectInputStream.readObject();
    } catch (IOException | ClassNotFoundException e) {
        e.printStackTrace();
        return null;
    }
}

Java原生序列化底层实现原理

其实答案很明显就在ObjectOutputStream.writeObject这个方法中了,我们来看一下源码:

writeObject(obj)调用了 writeObject0(obj, false);

//     1. String                             writeString
//     2. 数组                                writeArray
//     3. 枚举类                              writeEnum
//     4. 实现了Serializable的类               writeOrdinaryObject
    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 {
        // 抛异常
    }

我们关注writeOrdinaryObject(obj, desc, unshared);这个方法

    /**
     * TC_OBJECT 用于标识序列化流里下一个是个对象 new Object.
     * final static byte TC_OBJECT =       (byte)0x73;
     */
bout.writeByte(TC_OBJECT);
writeClassDesc(desc, false);
handles.assign(unshared ? null : obj);
if (desc.isExternalizable() && !desc.isProxy()) {
    writeExternalData((Externalizable) obj);
} else {
    writeSerialData(obj, desc);
}

再看writeSerialData方法

private void writeSerialData(Object obj, ObjectStreamClass desc)
    {
        ObjectStreamClass.ClassDataSlot[] slots = desc.getClassDataLayout();
        for (int i = 0; i < slots.length; i++) {
            ObjectStreamClass slotDesc = slots[i].desc;
            if (slotDesc.hasWriteObjectMethod()) {
               // .....  这里是 如果你有writeObject方法,就反射调用你自己的方法
            } else {
                defaultWriteFields(obj, slotDesc);
                // 否则 defaultWriteFields
            }
        }
    }

接下来遍历所有字段,如果是基本数据类型,通过反射获取他们的值序列化;否则,递归调用 writeObject0。

实现原理小结

当然你的类重写了writeObject,就反射invoke你重写的方法

如果对象实现了Serializable接口,就调用writeOrdinaryObject()方法。

会通过反射拿到 序列化对象的所有字段的值并写入

serialVersionUID的作用

private static final long serialVersionUID = 1905122041950251207L;

标明当前class的版本号。保持版本的兼容性。反序列化时会验证字节流的serialVersionUID与本地类是否相同,不同会出现序列化版本不一致的异常。如果不显示定义,会由JVM依据class的信息自动生成。对这个类编译两次,他们的serialVersionUID可能会不同,由此会造成反序列化报错。(具体如何生成serialVersionUID后文会分析)因此必须要显示定义serialVersionUID。idea插件可以帮助自动生成serialVersionUID,可以参考:实体类中如何自动生成serialVersionUID

什么字段不会参与序列化

  • final,static修饰的
  • @Transient注解修饰的
  • 序列化不会关心Method,Constructor,只关心对象特有的Field。
  • socket,thread类不能也没有必要序列化。
特殊的serialVersionUID

serialVersionUID用static修饰,会参与序列化吗?不会

想要serialVersionUID起到标明当前class的版本号的作用,需要满足如下条件:

  • 变量名字必须为 "serialVersionUID"
  • 必须被staticfinal 关键字修饰
  • 必须是个 long 类型的变量

因此,serialVersionUID 只是用来被 JVM 识别,实际并没有被序列化

那serialVersionUID如何生效呢?即如何拿到字节流的serialVersionUID?

serialVersionUID是通过计算得到的,通过对类,超类,接口,域类型和方法签名按照规范方式排序,然后进行SHA得到的20字节长度的数据。 因此,理论上来说当对象所属的类的定义发生变化时,其serialVersionUID一定会发生变化,但是由于序列化机制只使用SHA码的前8个字节,因此不是一定发生变化,但是几率还是非常大的。

serialVersionUID能修改吗

serialVersionUID用于标明当前class的版本号,那如果修改了类,到底该不该修改serialVersionUID呢?改了把,以前的字节流无法正确反序列化;不改吧,旧版本class的兼容性问题该怎么处理呢?

阿里是这样规定的:

【强制】序列化类新增属性时,请不要修改 serialVersionUID 字段,避免反序列失败;如果 完全不兼容升级,避免反序列化混乱,那么请修改 serialVersionUID 值。 说明:注意 serialVersionUID 不一致会抛出序列化运行时异常。

简单来说,就是如果新增字段不影响核心流程,能做到兼容,那就尽量去兼容。如果新增字段非常重要,完全无法兼容,那就没办法了,只能修改。还是根据具体情况来判断。

序列化实现深拷贝

    public static <T extends Serializable> T deepCopy(T object) {
        try {
            ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
            ObjectOutputStream objectOutputStream = new                                                     ObjectOutputStream(byteArrayOutputStream);
            // writeObject 方法
            objectOutputStream.writeObject(object);
            objectOutputStream.flush();
            ByteArrayInputStream byteArrayInputStream = new                                                 ByteArrayInputStream(byteArrayOutputStream.toByteArray());
            ObjectInputStream objectInputStream = new                                                       ObjectInputStream(byteArrayInputStream);
            // readObject 方法
            return (T) objectInputStream.readObject();
        } catch (IOException | ClassNotFoundException e) {
            e.printStackTrace();
            return null;
        }
    }

参考文献

Java 序列化详解

java反序列化的原理_Java 序列化和反序列化的底层原理