在此之前,对于 Java 中的序列化,我一直停留在使用层面 —— 把需要序列化在网络上传输的类实现Serializable
接口就可以了
但对于这块知识点,随着工作年限的提升,我觉得必须要好好研究下它了,不能似懂非懂的只知道使用。本文将针对以下几个知识点深入讲解Serializable
接口:
Serializable
接口?Serializable
接口也能成功序列化?serialVersionUID
,这个属性的作用是什么?Java 序列化是 JDK 1.1 时引入的一组开创性的特性,用于将 Java 对象转换为字节数组,便于存储或传输。此后,仍然可以将字节数组转换回 Java 对象原有的状态
序列化的思想是“冻结”对象状态,然后写到磁盘或者在网络中传输;反序列化 的思想是“解冻”对象状态,重新获得可用的 Java 对象
序列化:Java 对象转换成可在网络中传输或可写入磁盘的一个字节序列
反序列化:将一个字节序列转换为 Java 对象,便于在程序中使用
序列化有一条规则,就是要序列化的对象必须实现Serializbale
接口,否则就会报 NotSerializableException 异常
我们先来看看Serializable
接口的定义:
public interface Serializable {
}
我们发现,只是定义了接口,但内部没有任何代码。那它是怎么能够保证实现了它的类对象能够被正常的序列化和反序列化呢?
为了能够知道 Java 底层对于序列化、反序列化的实现,我们先定义一个类(Woman)
/**
* @Author: ZhangGongMing
* @CreateTime: 2025/5/13 13:57
* @Description:
* @Version: 1.0
*/
@Data
public class Woman {
private String name;
private String age;
}
接着创建一个测试类,通过ObjectOutputStream
将“18 岁的王二”写入到文件当中,实际上就是一种序列化的过程;再通过ObjectInputStream
将“18 岁的王二”从文件中读出来,实际上就是一种反序列化的过程
/**
* @Author: ZhangGongMing
* @CreateTime: 2025/5/13 14:12
* @Description:
* @Version: 1.0
*/
public class Main {
public static void main(String[] args) {
Woman woman = new Woman();
woman.setAge("18");
woman.setName("张三");
System.out.println(woman);
// 序列化过程:将对象写入到文件
try (ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("text"))) {
oos.writeObject(woman);
} catch (IOException e) {
e.printStackTrace();
}
// 反序列化过程:从文件中读出对象
try (ObjectInputStream ois = new ObjectInputStream(new FileInputStream("text"))) {
Woman wm = (Woman) ois.readObject();
System.out.println(wm);
} catch (IOException | ClassNotFoundException e) {
e.printStackTrace();
}
}
}
我们运行发现,第一个打印能成功执行,但序列化时报错,原因在于我们的Woman
类没有实现 Serializable
接口。所以在运行的时候会抛出异常。堆栈信息如下:
java.io.NotSerializableException: com.zhang.model.Woman
at java.io.ObjectOutputStream.writeObject0(ObjectOutputStream.java:1184)
at java.io.ObjectOutputStream.writeObject(ObjectOutputStream.java:348)
at com.zhang.Main.main(Main.java:22)
顺着堆栈信息,我们看一下 ObjectOutputStream 的 writeObject0 方法。其部分源码如下:
// 判断对象是否为字符串类型,如果是,则调用 writeString 方法进行序列化
if (obj instanceof String) {
writeString((String) obj, unshared);
}
// 判断对象是否为数组类型,如果是,则调用 writeArray 方法进行序列化
else if (cl.isArray()) {
writeArray(obj, desc, unshared);
}
// 判断对象是否为枚举类型,如果是,则调用 writeEnum 方法进行序列化
else if (obj instanceof Enum) {
writeEnum((Enum>) obj, desc, unshared);
}
// 判断对象是否为可序列化类型,如果是,则调用 writeOrdinaryObject 方法进行序列化
else if (obj instanceof Serializable) {
writeOrdinaryObject(obj, desc, unshared);
} else {
// 如果对象不能被序列化,则抛出 NotSerializableException 异常
if (extendedDebugInfo) {
throw new NotSerializableException(
cl.getName() + "\n" + debugInfoStack.toString());
} else {
throw new NotSerializableException(cl.getName());
}
}
也就是说,ObjectOutputStream 在序列化过程中,会先判断被序列化的对象是哪一种类型,字符串?数组?枚举?还是 Serializable。如果都不是则抛出 NotSerializableException 异常
加入Woman
类实现了Serializable
接口,就可以成功序列化和反序列化了
/**
* @Author: ZhangGongMing
* @CreateTime: 2025/5/13 13:57
* @Description:
* @Version: 1.0
*/
@Data
public class Woman implements Serializable {
private String name;
private String age;
}
其内部是怎么实现的序列化和反序列化呢?
以 ObjectOutputStream 为例,他在序列化时会依次调用:
writeObject()
→writeObject0()
→writeOrdinaryObject()
→writeSerialData()
→invokeWriteObject()
→defaultWriteFields()
private void defaultWriteFields(Object obj, ObjectStreamClass desc) throws IOException {
// 获取对象的类,并检查是否可以进行默认的序列化
Class> cl = desc.forClass();
desc.checkDefaultSerialize();
// 获取对象的基本类型字段的数量,以及这些字段的值
int primDataSize = desc.getPrimDataSize();
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++) {
// 调用 writeObject0 方法将对象的非基本类型字段序列化写入输出流
try {
writeObject0(objVals[i], fields[numPrimFields + i].isUnshared());
}
// 如果在写入过程中出现异常,则将异常包装成 IOException 抛出
catch (IOException ex) {
if (abortIOException == null) {
abortIOException = ex;
}
}
}
}
那怎么反序列化呢?
以 ObjectInputStream 为例,他它在反序列化的时候会依次调用:
readObject()
→readObject0()
→readOrdinaryObject()
→readSerialData()
→defaultReadFields()
private void defaultReadFields(Object obj, ObjectStreamClass desc) throws IOException {
// 获取对象的类,并检查对象是否属于该类
Class> cl = desc.forClass();
if (cl != null && obj != null && !cl.isInstance(obj)) {
throw new ClassCastException();
}
// 获取对象的基本类型字段的数量和值
int primDataSize = desc.getPrimDataSize();
if (primVals == null || primVals.length < primDataSize) {
primVals = new byte[primDataSize];
}
// 从输入流中读取基本类型字段的值,并存储在 primVals 数组中
bin.readFully(primVals, 0, primDataSize, false);
if (obj != null) {
// 将 primVals 数组中的基本类型字段的值设置到对象的相应字段中
desc.setPrimFieldValues(obj, primVals);
}
// 获取对象的非基本类型字段的数量和值
int objHandle = passHandle;
ObjectStreamField[] fields = desc.getFields(false);
Object[] objVals = new Object[desc.getNumObjFields()];
int numPrimFields = fields.length - objVals.length;
// 循环读取对象的非基本类型字段的值
for (int i = 0; i < objVals.length; i++) {
// 调用 readObject0 方法读取对象的非基本类型字段的值
ObjectStreamField f = fields[numPrimFields + i];
objVals[i] = readObject0(Object.class, f.isUnshared());
// 如果该字段是一个引用字段,则将其标记为依赖该对象
if (f.getField() != null) {
handles.markDependency(objHandle, passHandle);
}
}
if (obj != null) {
// 将 objVals 数组中的非基本类型字段的值设置到对象的相应字段中
desc.setObjFieldValues(obj, objVals);
}
passHandle = objHandle;
}
我想看到这里,第一个问题的答案是不是已经有答案了。Serializable
接口之所以定义为空,是因为它只是起一个标记作用。告诉 JVM 实现了它的对象是可以被序列化的。而具体的序列化和反序列化操作并不需要它来完成
在使用 Java 自带的序列化方式时,必须要显示的使用 Serializable 接口指定具体需要执行序列化操作的类
但在实际开发中,我们很少使用 JDK 自带的序列化和反序列化。原因如下:
这也解释了为什么我的项目不使用 Serializable 指定类但是能够成功序列化?
因为我没有使用 Java 自身的序列化方式,但是还是建议按照规范在每个需要序列化的类上实现 Serializable 接口。因为你不能保证你们的系统未来不换序列化方式。加入未来换成了 Java 的序列化方式,没有 Serializable 注解会导致程序抛出异常!
使用 static 和 transient 修饰的字段是不会被序列化的
我们在 Woman 类中增加两个字段
/**
* @Author: ZhangGongMing
* @CreateTime: 2025/5/13 13:57
* @Description:
* @Version: 1.0
*/
@Data
public class Woman implements Serializable {
private String name;
private String age;
// 不会参与序列化
private static String address = "银河系";
private transient String son = "张三";
@Override
public String toString() {
return "Woman{" + "name=" + name + ",age=" + age + ",address=" + address + ",son=" + son + "}";
}
}
修改测试类,在序列化之后反序列以前修改属性,观察 static 修饰的变量
/**
* @Author: ZhangGongMing
* @CreateTime: 2025/5/13 14:12
* @Description:
* @Version: 1.0
*/
public class Main {
public static void main(String[] args) {
Woman woman = new Woman();
woman.setAge("18");
woman.setName("张三");
System.out.println(woman);
// 序列化过程:将对象写入到文件
try (ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("text"))) {
oos.writeObject(woman);
} catch (IOException e) {
e.printStackTrace();
}
Woman.address = "中国";
// 反序列化过程:从文件中读出对象
try (ObjectInputStream ois = new ObjectInputStream(new FileInputStream("text"))) {
Woman wm = (Woman) ois.readObject();
System.out.println(wm);
} catch (IOException | ClassNotFoundException e) {
e.printStackTrace();
}
}
}
输出:
Woman{name=张三,age=18,address=银河系,son=张三}
Woman{name=张三,age=18,address=中国,son=null}
从结果的对比中,我们发现:
为什么呢?因为序列化保存的是对象的状态,而static
修饰的字段属于类的状态,因此可以证明序列化并不保存static
所修饰的字段
transient
的中文释义为“临时的”。它可以阻止字段被序列化到文件中。在被反序列化后,transient
字段的值被设为初始值,比如int
型的初始值为 0,对象型的初始值为null
private static ObjectStreamField[] getDefaultSerialFields(Class> cl) {
// 获取该类中声明的所有字段
Field[] clFields = cl.getDeclaredFields();
ArrayList list = new ArrayList<>();
int mask = Modifier.STATIC | Modifier.TRANSIENT;
// 遍历所有字段,将非 static 和 transient 的字段添加到 list 中
for (int i = 0; i < clFields.length; i++) {
Field field = clFields[i];
int mods = field.getModifiers();
if ((mods & mask) == 0) {
// 根据字段名、字段类型和字段是否可序列化创建一个 ObjectStreamField 对象
ObjectStreamField osf = new ObjectStreamField(field.getName(), field.getType(), !Serializable.class.isAssignableFrom(cl));
list.add(osf);
}
}
int size = list.size();
// 如果 list 为空,则返回一个空的 ObjectStreamField 数组,否则将 list 转换为 ObjectStreamField 数组并返回
return (size == 0) ? NO_FIELDS :
list.toArray(new ObjectStreamField[size]);
}
看到Modifier.STATIC | Modifier.TRANSIENT
了吧,这两个修饰符标记的字段就没有被放入要序列化的字段列表中
我们在实现了 Serializable 接口后,IDEA 往往会提示我们定义一个serialVersionUID
。那这个 serialVersionUID
到底是什么呢?
serialVersionUID
被称为序列化 ID。它是决定 Java 对象能否反序列化成功的重要因子。在反序列化时,JVM 会把字节流中的serialVersionUID
与被序列化类中的serialVersionUID
进行比较,如果相同则可以进行反序列化,否则就会抛出序列化版本不一致的异常
生成序列化 ID 我们常见的有以下三种方式:
private static final long serialVersionUID = 1L。
private static final long serialVersionUID = -2095916884810199532L;
@SuppressWarnings
注解@SuppressWarnings("serial")
比较常见的其实是前两种,第三种可能比较少见:使用@SuppressWarnings("serial")
注解时,该注解会为被序列化类自动生成一个随机的序列化 ID
Java 虚拟机是否允许反序列化,不仅取决于类路径和功能代码是否一致,还有一个非常重要的因素就是序列化 ID 是否一致
也就是说,如果没有特殊需求,采用默认的序列化 ID(1L)就可以,这样可以确保代码一致时反序列化成功
这样,开篇提到的第三个问题也得到了很好的解释。总结下其实serialVersionUID
只是 JVM 在反序列时进行版本一致性判断的属性