JAVA反序列化基础

1.what is

序列化与反序列化

Java序列化是指把Java对象转换为字节序列的过程;

Java反序列化是指把字节序列恢复为Java对象的过程。

序列化分为两大部分:序列化和反序列化。序列化是这个过程的第一部分,将数据分解成字节流,以便存储在文件中或在网络上传输。反序列化就是打开字节流并重构对象。对象序列化不仅要将基本数据类 型转换成字节表示,有时还要恢复数据。恢复数据要求有恢复数据的对象实例。

2.why need

从两方面讨论

  • 第一个是存储

​ 内存中的对象是临时的,进程关闭就消失,为了实现了数据的持久化,通过序列化可以把数据永久地保存到硬盘上,可以把内存中的对象保存到一个文件中或者数据库中。需要的时候再把字节序列还原成原来的Java对象

  • 第二个是通信

    我们知道,当两个进程进行远程通信时,可以相互发送各种类型的数据,包括文本、图片、音频、视频等, 而这些数据都会以二进制序列的形式在网络上传送。那么当两个Java进程进行通信时,能否实现进程间的对象传送呢?答案是可以的。如何做到呢?这就需要Java序列化与反序列化了.一方面,发送方需要把这个Java对象转换为字节序列,然后在网络上传送;另一方面,接收方需要从字节序列中恢复出Java对象。

    这样一来,就可以通过RMI传输对象了

3.几种常见的序列化和反序列化协议

XML 是一种常用的序列化和反序列化协议,具有跨机器,跨语言等优点

SOAP(Simple Object Access protocol) 是一种被广泛应用的,基于XML为序列化和反序列化协议的结构化消息传递协议,就是一套基于XML序列化协议的、严格规定了“如何在网络上传输结构化数据”的通信规则

JSON(Javascript Object Notation)
Protobuf

4.序列化实现

只有实现了Serializable或者Externalizable接口的类的对象才能被序列化为字节序列(不是则会抛出异常)

image-20260317182906287

Serializable 接口

是 Java 提供的序列化接口,它是一个空接口

1
2
public interface Serializable {
}

Serializable 用来标识当前类可以被 ObjectOutputStream 序列化,以及被 ObjectInputStream 反序列化。(ObjectOutputStream和ObjectInputStream是专门用于对象序列化/反序列化的核心类)

Serializable 接口的基本使用

通过 ObjectOutputStream 将需要序列化数据写入到流中,因为 Java IO 是一种装饰者模式,因此可以通过 ObjectOutStream 包装 FileOutStream 将数据写入到文件中或者包装 ByteArrayOutStream 将数据写入到内存中。同理,可以通过 ObjectInputStream 将数据从磁盘 FileInputStream 或者内存 ByteArrayInputStream 读取出来然后转化为指定的对象即可。
image-20260317190558819

Serializable 接口的特点

1.序列化类的属性没有实现 Serializable 那么在序列化就会报错

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
import java.io.*;

// 测试主类
public class SerializableTest {
public static void main(String[] args) {
// 创建包含未序列化属性的Student对象
Student student = new Student("小明", 20, new Color("红色"));

// 序列化Student对象到文件
try (ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("student_error.dat"))) {
oos.writeObject(student); // 执行序列化,此处会抛出异常
System.out.println("序列化成功");
} catch (IOException e) {
e.printStackTrace(); // 打印异常信息
}
}
}

// 实现了Serializable的Student类
class Student implements Serializable {
//是 Java 中用于序列化版本控制的核心常量
private static final long serialVersionUID = 1L;
private String name;
private int age;
// 引用了未实现Serializable的Color类
private Color color;

public Student(String name, int age, Color color) {
this.name = name;
this.age = age;
this.color = color;
}
}

// 未实现Serializable的Color类(核心问题点)
class Color {
private String colorName;

public Color(String colorName) {
this.colorName = colorName;
}
}

image-20260317200415090

序列化Student对象时,JVM 递归检查到其color属性所属的Color类未实现Serializable接口,违反了Java序列化的规则。

2.在反序列化过程中,它的父类如果没有实现序列化接口,那么将需要提供无参构造函数 来重新创建对象。

这里借用一个师傅的代码了

Animal 是父类,它没有实现 Serilizable 接口

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public class Animal {
private String color;

public Animal() {//没有无参构造将会报错
System.out.println("调用 Animal 无参构造");
}

public Animal(String color) {
this.color = color;

System.out.println("调用 Animal 有 color 参数的构造");
}

@Override
public String toString() {
return "Animal{" +
"color='" + color + '\'' +
'}';
}
}

BlackCat 是 Animal 的子类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public class BlackCat extends Animal implements Serializable {
private static final long serialVersionUID = 1L;
private String name;

public BlackCat() {
super();
System.out.println("调用黑猫的无参构造");
}

public BlackCat(String color, String name) {
super(color);
this.name = name;
System.out.println("调用黑猫有 color 参数的构造");
}

@Override
public String toString() {
return "BlackCat{" +
"name='" + name + '\'' +super.toString() +'\'' +
'}';
}
}

SuperMain 测试类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
public class SuperMain {
private static final String FILE_PATH = "./super.bin";

public static void main(String[] args) throws Exception {
serializeAnimal();
deserializeAnimal();
}

private static void serializeAnimal() throws Exception {
BlackCat black = new BlackCat("black", "我是黑猫");
System.out.println("序列化前:"+black.toString());
System.out.println("=================开始序列化================");
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream(FILE_PATH));
oos.writeObject(black);
oos.flush();
oos.close();
}

private static void deserializeAnimal() throws Exception {
System.out.println("=================开始反序列化================");
ObjectInputStream ois = new ObjectInputStream(new FileInputStream(FILE_PATH));
BlackCat black = (BlackCat) ois.readObject();
ois.close();
System.out.println(black);
}
}

输出结果

1
2
3
4
5
6
7
调用 Animal 有 color 参数的构造
调用黑猫有 color 参数的构造
序列化前:BlackCat{name='我是黑猫'Animal{color='black'}'}
=================开始序列化================
=================开始反序列化================
调用 Animal 无参构造
BlackCat{name='我是黑猫'Animal{color='null'}'}

从上面的执行结果来看,如果要序列化的对象的父类 Animal 没有实现序列化接口,那么在反序列化时是会调用对应的无参构造方法的,这样做的目的是重新初始化父类的属性,例如 Animal 因为没有实现序列化接口,因此对应的 color 属性就不会被序列化,因此反序列得到的 color 值就为 null。
3. 一个实现 Serializable 接口的子类也是可以被序列化的。

4. 静态成员变量是不能被序列化

序列化是针对对象属性的,而静态成员变量是属于类的。

5. transient 标识的对象成员变量不参与序列化

在下面这个栗子中,MyList 这个类定义了一个 arr 数组属性,初始化的数组长度为 100。在实际序列化时如果让 arr 属性参与序列化的话,那么长度为 100 的数组都会被序列化下来,但是我在数组中可能只存放 30 个数组而已,这明显是不可理的,所以这里就要自定义序列化过程啦,具体的做法是写以下两个 private 方法:

1
2
private void writeObject(java.io.ObjectOutputStream s)throws java.io.IOException
private void readObject(java.io.ObjectInputStream s)throws java.io.IOException, ClassNotFoundException

从这两个方法的名字就可以看出分别是序列化写入数据和反序列化读取数据用的,那么这两个方法是在哪里使用呢?其实在序列化和反序列化过程中会通过反射调用的,具体下面会分析这个过程哦。

分析一下这个 transient 应用:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
public class MyList implements Serializable {

private String name;


/*
transient 表示该成员 arr 不需要被序列化
*/
private transient Object[] arr;

public MyList() {
}

public MyList(String name) {
this.name = name;
this.arr = new Object[100];
/*
给前面30个元素进行初始化
*/
for (int i = 0; i < 30; i++) {
this.arr[i] = i;
}
}

@Override
public String toString() {
return "MyList{" +
"name='" + name + '\'' +
", arr=" + Arrays.toString(arr) +
'}';
}


//-------------------------- 自定义序列化反序列化 arr 元素 ------------------

/**
* Save the state of the <tt>ArrayList</tt> instance to a stream (that
* is, serialize it).
*
* @serialData The length of the array backing the <tt>ArrayList</tt>
* instance is emitted (int), followed by all of its elements
* (each an <tt>Object</tt>) in the proper order.
*/
private void writeObject(java.io.ObjectOutputStream s)
throws java.io.IOException {
// 步骤1:执行JVM默认的序列化逻辑,保存类中普通的可序列化字段(非transient、非static的字段)
s.defaultWriteObject();

// 步骤2:手动序列化 arr 数组的前30个元素(补充默认序列化未处理的内容)
//手动序列化 arr 前面30个元素
for (int i = 0; i < 30; i++) {
s.writeObject(arr[i]);
}
}

/**
* Reconstitute the <tt>ArrayList</tt> instance from a stream (that is,
* deserialize it).
*/
private void readObject(java.io.ObjectInputStream s)
throws java.io.IOException, ClassNotFoundException {
// 步骤1:执行JVM默认的反序列化逻辑,恢复类中普通的序列化字段(比如非transient的成员变量)
s.defaultReadObject();
// 步骤2:手动初始化数组arr,长度固定为30
arr = new Object[30];

// 步骤3:循环读取30个对象,依次存入arr数组
// Read in all elements in the proper order.
for (int i = 0; i < 30; i++) {
arr[i] = s.readObject();
}
}
}

测试

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
public class TransientMain {
private static final String FILE_PATH = "./transient.bin";
public static void main(String[] args) throws Exception {
serializeMyList();

deserializeMyList();
}

private static void serializeMyList() throws Exception {
System.out.println("序列化...");
MyList myList = new MyList("ArrayList");
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream(FILE_PATH));
oos.writeObject(myList);
oos.flush();
oos.close();
}

/*
1.如果 private Object[] arr; 没有使用 transient ,那么整个数组都会被保存,而不是保存实际存储的数据
输出结果:MyList{name='ArrayList', arr=[0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null]}
2.private transient Object[] arr;设置了 transient,表示 arr 元素不进行序列化
输出结果:MyList{name='ArrayList', arr=null}
3.参考 ArrayList 处理内部的 transient Object[] elementData; 数组是通过 writeObject 和 readObject 实现的
我们的 MyList 内部也可以借鉴这种方式实现transient元素的手动序列化和反序列化。
*/
private static void deserializeMyList() throws Exception {
System.out.println("反序列化...");
ObjectInputStream ois = new ObjectInputStream(new FileInputStream(FILE_PATH));
MyList myList = (MyList) ois.readObject();
ois.close();
System.out.println(myList);
}
}

测试输出结果

image-20260318213408086

6.Serializable 在序列化和反序列化过程中大量使用了反射,因此其过程会产生的大量的内存碎片

serialVersionUID与兼容性问题

Serializable 与反射 / 内存碎片:实现 Serializable 接口的类,JVM 会通过反射机制遍历类的字段、方法来完成序列化(对象转字节)和反序列化(字节转对象)。反射会频繁创建临时对象,这些小对象回收后就会产生内存碎片,这也是为什么高并发场景会推荐用 Protostuff、Kryo 等替代原生序列化的原因。

序列化的核心目的就是把内存中的对象 “存起来”(比如写文件、传网络),之后再 “还原” 成对象,而 serialVersionUID (一个 long 类型的常量)就是这个 “存 或者取” 过程中的 “身份验证标识”。

5.序列化ID

还是上面提到的,在进行序列化时,需要有一个serialVersionUID字段,这便是序列化ID

1
private static final long serialVersionUID = 1L;

这个序列化ID起着关键的作用,它决定着是否能够成功反序列化!java的序列化机制是通过判断运行时类的serialVersionUID来验证版本一致性的,在进行反序列化时,JVM会把传进来的字节流中的serialVersionUID与本地实体类中的serialVersionUID进行比较,如果相同则认为是一致的,便可以进行反序列化,否则就会报序列化版本不一致的异常。

即序列化ID是为了保证成功进行反序列化

如何生成这个 serialVersionUID呢?

  1. 使用 AS plugin 插件就可以生成
  2. 在JDK中,可以利用 JDK 的 bin 目录下的 serialver 工具产生这个serialVersionUID,对于 Student.class,执行命令:serialver com.example.seriable.Student