序列化和反序列化

!!! note 目录

序列化和反序列化

一、Java 序列化和反序列化概念

1.1 序列化 (Serialization)

序列化是将Java对象的状态转换为字节流的过程,以便能够将对象的状态保存到存储介质(如文件、数据库)或通过网络传输到其他Java虚拟机中。序列化使得对象能够被持久化和传输。

1.2 反序列化 (Deserialization)

反序列化是将字节流恢复成相应Java对象的过程。通过反序列化,可以从存储介质或网络中恢复对象的状态,使其重新变为可操作的Java对象。

1.3 序列化和反序列化的主要用途

  1. 持久化存储:将对象的状态保存到文件或数据库中,以便在以后恢复使用。
  2. 网络传输:通过网络将对象传输到远程主机上。
  3. 深度克隆:通过序列化和反序列化可以创建对象的深度拷贝。

1.4 序列化和反序列化的示例

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
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
package com.camellia.io.ObjectOutputStream;

import org.junit.jupiter.api.Test;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.ObjectOutputStream;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;

/**
* {@code ObjectOutputStreamTest} 类演示了使用 {@link ObjectOutputStream}
* 将Java对象序列化到文件中。
* <p>
* {@code ObjectOutputStream} 类用于对象的序列化,即将Java对象转换为可以
* 写入文件或通过网络传输的字节流。
* </p>
* <p>
* 序列化是将对象转换为字节序列的过程,这样可以将其存储在文件中、
* 通过网络传输或保存到数据库中。序列化的对象可以在以后反序列化,
* 即将字节序列转换回Java对象。
* </p>
*/
public class ObjectOutputStreamTest {

/**
* 测试方法,将一个 {@link Date} 对象序列化到文件中。
* <p>
* 该方法创建一个 {@code ObjectOutputStream},写入到 "src/document/object" 文件。
* 然后序列化一个 {@code Date} 对象,并将其写入文件。
* </p>
*/
@Test
public void testObjectOutputStream() {
// 对象字节输出流,用于序列化对象
ObjectOutputStream oos = null;
try {
// 创建一个写入 "src/document/object" 文件的 ObjectOutputStream
oos = new ObjectOutputStream(new FileOutputStream("src/document/object"));
// 创建一个新的 Date 对象准备序列化
Date newTime = new Date();
// 序列化 Date 对象
oos.writeObject(newTime);
// 刷新流,确保所有数据被写入文件
oos.flush();
} catch (IOException e) {
e.printStackTrace();
} finally {
// 关闭 ObjectOutputStream 以释放资源
if (oos != null) {
try {
oos.close();
} catch (IOException e) {
throw new RuntimeException(e);
}
}
}
}

/**
* 测试方法,将多个 {@link Date} 对象序列化到文件中。
* <p>
* 该方法创建一个 {@code ObjectOutputStream},写入到 "src/document/dates" 文件。
* 然后序列化一个包含多个 {@code Date} 对象的 {@code List} 并将其写入文件。
* </p>
*/
@Test
public void testManyObjectOutputStream() {
// 对象字节输出流,用于序列化对象
ObjectOutputStream oos = null;
try {
// 创建一个写入 "src/document/dates" 文件的 ObjectOutputStream
oos = new ObjectOutputStream(new FileOutputStream("src/document/dates"));
// 创建多个 Date 对象
Date newTime1 = new Date();
Date newTime2 = new Date();
Date newTime3 = new Date();
Date newTime4 = new Date();
// 将 Date 对象添加到列表中
List<Date> list = new ArrayList<>();
list.add(newTime1);
list.add(newTime2);
list.add(newTime3);
list.add(newTime4);
// 序列化 Date 对象列表
oos.writeObject(list);
// 刷新流,确保所有数据被写入文件
oos.flush();
} catch (IOException e) {
e.printStackTrace();
} finally {
// 关闭 ObjectOutputStream 以释放资源
if (oos != null) {
try {
oos.close();
} catch (IOException e) {
throw new RuntimeException(e);
}
}
}
}
}

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
package com.camellia.io.ObjectInputStream;

import org.junit.jupiter.api.Test;

import java.io.FileInputStream;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.util.Date;
import java.util.List;

/**
* {@code ObjectInputStreamTest} 类演示了使用 {@link ObjectInputStream}
* 将文件中的字节流反序列化为Java对象。
* <p>
* {@code ObjectInputStream} 类专门用于反序列化,即将字节序列转换为JVM中的Java对象。
* </p>
*/
public class ObjectInputStreamTest {

/**
* 测试方法,从文件中反序列化一个 {@link Date} 对象。
* <p>
* 该方法创建一个 {@code ObjectInputStream},读取 "src/document/object" 文件中的字节流,
* 并将其反序列化为一个 {@code Date} 对象。
* </p>
*/
@Test
public void testObjectInputStream() {
// 使用try-with-resources语句,自动关闭资源
try (ObjectInputStream ois = new ObjectInputStream(new FileInputStream("src/document/object"))) {
// 读取并反序列化对象
Object object = ois.readObject();
System.out.println(object);
} catch (IOException e) {
e.printStackTrace();
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
}

/**
* 测试方法,从文件中反序列化一个包含多个 {@link Date} 对象的列表。
* <p>
* 该方法创建一个 {@code ObjectInputStream},读取 "src/document/dates" 文件中的字节流,
* 并将其反序列化为一个 {@code List<Date>} 对象。
* </p>
*/
@Test
public void testManyObjectInputStream() {
// 使用try-with-resources语句,自动关闭资源
try (ObjectInputStream ois = new ObjectInputStream(new FileInputStream("src/document/dates"))) {
// 读取并反序列化对象列表
List<Date> dates = (List<Date>) ois.readObject();
for (Date date : dates) {
System.out.println(date);
}
} catch (IOException e) {
e.printStackTrace();
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
}
}

二、自定义序列化和反序列化

2.1 关键点和注意事项

  1. 实现Serializable接口:要使一个类的对象可以被序列化,该类必须实现java.io.Serializable接口。

    • 当java程序中类实现了Serializable接口,编译器会自动给该类添加一个“序列化版本号”。
    • 序列化版本号:serialVersionUID
  2. serialVersionUID:这是一个唯一标识符,用于验证在反序列化时,确保加载的类与序列化时保存的类是兼容的。建议手动定义serialVersionUID以避免版本不兼容问题。

    1
    private static final long serialVersionUID = 1L;
    • 使用 @Serial注解可以辅助判断serialVersionUID是否写错。

      如果不手动定义 serialVersionUID,编译器会根据类的结构自动生成一个。这些生成的值依赖于类的字段和方法等细节。
      即使类的小幅度变动(如添加新字段),编译器可能也会生成一个新的 serialVersionUID,可能影响序列化的兼容性。

  3. transient关键字:标记为transient的字段不会被序列化。

    1
    private transient int transientField;
  4. 静态字段:静态字段属于类而不属于对象,因此不会被序列化。

  5. 自定义序列化:如果需要控制对象的序列化过程,可以实现writeObjectreadObject方法。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    private void writeObject(ObjectOutputStream oos) throws IOException {
    oos.defaultWriteObject();
    // 自定义序列化代码
    }

    private void readObject(ObjectInputStream ois) throws IOException, ClassNotFoundException {
    ois.defaultReadObject();
    // 自定义反序列化代码
    }
  6. 安全性:反序列化时可能会带来安全风险,例如反序列化恶意代码攻击,因此在处理不受信任的数据流时要特别小心。

2.2 示例代码

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
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
package com.camellia.io.ObjectOutputStream;

import java.io.Serial;
import java.io.Serializable;

/**
* {@code StudentSerialization} 类表示一个学生实体,具有姓名、年龄和ID等属性,并提供序列化功能。
*
* <p>实现 {@link Serializable} 接口表示此类的实例可以被序列化和反序列化。{@code serialVersionUID}
* 用于在反序列化过程中进行版本控制,确保序列化对象与反序列化时使用的类定义之间的兼容性。</p>
*
* <p>注意:建议显式定义 {@code serialVersionUID},以防止由于类结构的变化引起的问题。如果类定义发生更改,
* 而 {@code serialVersionUID} 没有相应更改,反序列化可能会失败或行为不可预测。</p>
*
* <p>为了保持序列化的兼容性,不同版本的类之间 {@code serialVersionUID} 值必须保持一致。如果自动计算出的值发生变化,
* 而类结构有显著变化,反序列化时可能会抛出 {@link java.io.InvalidClassException}。</p>
*
* <p>使用示例:</p>
* <pre>
* {@code
* StudentSerialization student = new StudentSerialization("张三", 20);
* }
* </pre>
*
* @author [Camellia.xiaohua]
*/
public class StudentSerialization implements Serializable {

/**
* 序列化版本标识符。此值必须在 {@code StudentSerialization} 类的不同版本之间保持一致。
* 如果此值发生变化,反序列化将无法与先前版本兼容,从而导致 {@link java.io.InvalidClassException}。
*/
@Serial
private static final long serialVersionUID = 9010515980424792363L;

private String name;
private int age;
private int id;

/**
* 构造一个新的 {@code StudentSerialization} 实例,并设置默认值。
*/
public StudentSerialization() {}

/**
* 构造一个新的 {@code StudentSerialization} 实例,并设置指定的姓名和年龄。
*
* @param name 学生的姓名
* @param age 学生的年龄
*/
public StudentSerialization(String name, int age) {
this.name = name;
this.age = age;
}

/**
* 返回学生的姓名。
*
* @return 学生的姓名
*/
public String getName() {
return name;
}

/**
* 设置学生的姓名。
*
* @param name 要设置的姓名
*/
public void setName(String name) {
this.name = name;
}

/**
* 返回学生的年龄。
*
* @return 学生的年龄
*/
public int getAge() {
return age;
}

/**
* 设置学生的年龄。
*
* @param age 要设置的年龄
*/
public void setAge(int age) {
this.age = age;
}

/**
* 返回 {@code StudentSerialization} 对象的字符串表示形式。
*
* @return 学生的字符串表示形式
*/
@Override
public String toString() {
return "Student{" +
"name='" + name + '\'' +
", age=" + age +
'}';
}
}

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
package com.camellia.io.ObjectOutputStream;

import org.junit.jupiter.api.Test;

import java.io.*;

/**
* {@code CustomSerialization} 类用于演示 Java 对象的序列化和反序列化过程。
*
* <p>通过此类中的测试方法,可以了解如何将 `StudentSerialization` 对象序列化到文件中,以及如何从文件中反序列化该对象。</p>
*/
public class CustomSerialization {

/**
* 测试序列化:将 {@code StudentSerialization} 对象序列化到文件中。
*
* <p>此方法创建一个 `StudentSerialization` 对象实例,并将其序列化到指定文件中。序列化后的文件可以用于后续的反序列化测试。</p>
*/
@Test
public void testSerialization() {
try (ObjectOutputStream oss = new ObjectOutputStream(new FileOutputStream("src/document/student"))) {
StudentSerialization student = new StudentSerialization("Camellia", 23);
oss.writeObject(student);
oss.flush(); // 确保所有数据都被写入文件
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
}

/**
* 测试反序列化:从文件中反序列化 `StudentSerialization` 对象。
*
* <p>此方法从序列化的文件中读取数据,并将其反序列化回 `StudentSerialization` 对象实例。</p>
*/
@Test
public void Deserialization() {
try (ObjectInputStream ois = new ObjectInputStream(new FileInputStream("src/document/student"))) {
Object object = ois.readObject();
if (object instanceof StudentSerialization) {
StudentSerialization student = (StudentSerialization) object;
System.out.println(student.toString());
}
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
}
}