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

序列化

什么是序列化

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

Java原生序列化

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

实现Serializable

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

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

实现Externalizable

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

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

Java原生序列化的使用

序列化其实就两个步骤:

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

反序列化也是类似

java 复制代码
// 这里以 序列化实现 深拷贝为例
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);

java 复制代码
//     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);这个方法

java 复制代码
    /**
     * 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方法

java 复制代码
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的作用

java 复制代码
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 不一致会抛出序列化运行时异常。

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

序列化实现深拷贝

java 复制代码
    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;
        }
    }

参考文献

从serialVersionUID到Java序列化

为什么不能轻易修改 serialVersionUID 字段

Java 序列化详解

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

相关推荐
reiraoy8 分钟前
缓存解决方案
java
安之若素^22 分钟前
启用不安全的HTTP方法
java·开发语言
ruanjiananquan9929 分钟前
c,c++语言的栈内存、堆内存及任意读写内存
java·c语言·c++
chuanauc1 小时前
Kubernets K8s 学习
java·学习·kubernetes
一头生产的驴1 小时前
java整合itext pdf实现自定义PDF文件格式导出
java·spring boot·pdf·itextpdf
YuTaoShao1 小时前
【LeetCode 热题 100】73. 矩阵置零——(解法二)空间复杂度 O(1)
java·算法·leetcode·矩阵
zzywxc7871 小时前
AI 正在深度重构软件开发的底层逻辑和全生命周期,从技术演进、流程重构和未来趋势三个维度进行系统性分析
java·大数据·开发语言·人工智能·spring
YuTaoShao4 小时前
【LeetCode 热题 100】56. 合并区间——排序+遍历
java·算法·leetcode·职场和发展
程序员张34 小时前
SpringBoot计时一次请求耗时
java·spring boot·后端
llwszx7 小时前
深入理解Java锁原理(一):偏向锁的设计原理与性能优化
java·spring··偏向锁