Java原生序列化机制,serialVersionUID详解
序列化
什么是序列化
将一个对象转化为字节流,从而能够保存到磁盘,进行网络传输。
Java原生序列化
Java原生序列化要求被序列化的类必须实现如下两个接口之一。
实现Serializable
没有任何实现,只是标志该类的对象是可序列化的。
还需要加上serialVersionUID,当然不加不会报错,至于为什么留到后文分析
实现Externalizable
Externalizable继承了Serializable接口,还定义了两个抽象方法:writeExternal()和readExternal()。
Externalizable强调必须重写writeExternal和readExternal方法,即必须自定义序列化方法,Serializable当然也可以自定义,但不是必须的。并且Externalizable要求类必须提供一个public的无参的构造方法。
Java原生序列化的使用
序列化其实就两个步骤:
- 创建一个对象输入流ObjectOutputStream
- 调用了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"
- 必须被
static
和final
关键字修饰 - 必须是个
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;
}
}
参考文献
转载自:https://juejin.cn/post/7277868881937924108