Java序列化和反序列化
# Java序列化和反序列化
# 什么是序列化和反序列化
序列化是指将对象的状态信息转换为可以存储或传输的形式的过程。在Java中,这通常意味着将对象转换为字节序列。
反序列化则是序列化的逆过程,它将字节序列恢复为对象。
在日常开发中,将对象转换为JSON字符串的过程确实可以视为序列化的一种形式。序列化是指将对象的状态信息转换为可以存储或传输的格式,而JSON字符串是一种常见的序列化格式,特别适合跨语言和平台的数据交换。
JSON 字符串转为对象则是反序列化。
# 使用场景
我们日常开发中,序列化和反序列化其实还是挺常见的,只不过有时候我们忘记了自己所做的事情其实就是序列化和反序列化。
举几个常见的场景:
- 网络传输:在分布式系统中,对象需要在网络上传输时,需要将对象序列化后发送,接收方再进行反序列化。
- 数据存储:将对象状态保存到文件或数据库中,以便后续恢复。
- 远程方法调用(RMI):在 Java 的 RMI 中,对象需要在客户端和服务器之间传递。
- 对象克隆:通过序列化和反序列化实现对象的深拷贝。
类似的场景其实很多。
不过就日常开发而言,可能大家在从 Redis 中存取对象、Dubbo 远程调用,这些场景可能会明确感知到序列化这件事,其他场景可能感受就不是特别明显。
# 实现序列化
# 实现Serializable 接口
假设有一个Student类,需要将其对象序列化到文件中。
import java.io.Serializable;
import java.io.FileOutputStream;
import java.io.ObjectOutputStream;
public class Student implements Serializable {
private static final long serialVersionUID = 1L; // 用于版本控制
private String name;
private int age;
// 构造函数、getter和setter省略
public static void main(String[] args) {
Student student = new Student("张三", 20);
try (FileOutputStream fos = new FileOutputStream("student.ser");
ObjectOutputStream oos = new ObjectOutputStream(fos)) {
oos.writeObject(student);
System.out.println("对象序列化成功!");
} catch (Exception e) {
e.printStackTrace();
}
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
# 实现 Externalizable 接口
与 Serializable 接口类似,但 Externalizable 接口提供了更灵活的序列化控制。
import java.io.Externalizable;
import java.io.FileOutputStream;
import java.io.ObjectOutput;
import java.io.ObjectOutputStream;
public class Employee implements Externalizable {
private String name;
private int age;
// 构造函数、getter和setter省略
@Override
public void writeExternal(ObjectOutput out) throws IOException {
out.writeObject(name);
out.writeInt(age);
}
@Override
public void readExternal(ObjectInput in) throws IOException, ClassNotFoundException {
name = (String) in.readObject();
age = in.readInt();
}
public static void main(String[] args) {
// 序列化与反序列化逻辑与Serializable类似,但会调用writeExternal和readExternal方法
}
}
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
相比于 Serializable,Externalizable 的灵活性主要体现在四点:
- Serializable 接口的序列化过程是由 JVM 自动完成的,不允许开发者对序列化过程进行自定义。Externalizable 接口要求开发者实现 writeExternal() 和 readExternal() 两个方法,从而完全控制序列化过程。这意味着开发者可以决定哪些字段需要序列化,哪些不需要,以及如何序列化这些字段。
- Serializable 的自动序列化过程虽然方便,但可能不是最高效的。Externalizable 允许开发者自定义序列化过程,因此可以针对特定需求进行优化。例如,可以只序列化必要的字段,或者采用更高效的数据结构来存储序列化数据,从而提高性能。
- Serializable 在默认情况下会序列化对象的所有非 transient 字段。如果对象的类结构发生变化如添加或删除字段,则可能会影响序列化和反序列化的兼容性。而对于 Externalizable 接口,开发者可以精确控制哪些字段被序列化,从而更容易地管理版本兼容性问题,甚至还可以在 writeExternal() 和 readExternal() 方法中添加逻辑来处理不同版本的序列化数据。
- 由于 Serializable 的序列化过程是自动的,因此可能会无意中序列化敏感信息(如密码、密钥等),此外,恶意用户还可能通过修改序列化数据来攻击系统。而 Externalizable 接口允许开发者明确控制哪些信息被序列化,从而可以减少敏感信息被泄露的风险,开发者甚至还可以在序列化过程中添加额外的安全措施(如加密、签名等)来提高系统的安全性。
# 使用 JSON 序列化库(如 Jackson、Gson)
import com.fasterxml.jackson.databind.ObjectMapper;
public class User {
private String username;
private int age;
// 构造函数、getter和setter省略
public static void main(String[] args) {
try {
User user = new User("李四", 30);
ObjectMapper mapper = new ObjectMapper();
String json = mapper.writeValueAsString(user);
System.out.println(json); // 输出JSON字符串
// 反序列化
User deserializedUser = mapper.readValue(json, User.class);
System.out.println(deserializedUser.getUsername()); // 输出:李四
} catch (Exception e) {
e.printStackTrace();
}
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
# static 和 transient
static 和 transient 是序列化时两个比较特殊的字段。
- static:
static
字段是类级别的,不属于对象实例,因此在序列化时不会被包含。 - transient:
transient
关键字修饰的字段在序列化过程中会被忽略,不会被序列化。
# 扩展
# DTO之类的类,必须实现Serializable接口?
虽然DTO
类并不强制要求实现Serializable
接口,但在许多场景中实现Serializable
接口是非常有用的。它可以帮助你在远程调用、对象持久化、会话管理和框架集成等场景中更方便地处理对象。
实现Serializable
接口可以使代码更加一致和可维护,为未来的扩展打下良好的基础。
假设我们有一个UserDTO
类,用于在客户端和服务端之间传递用户数据。
import java.io.Serializable;
public class UserDTO implements Serializable {
private static final long serialVersionUID = 1L;
private String username;
private String email;
private int age;
public UserDTO(String username, String email, int age) {
this.username = username;
this.email = email;
this.age = age;
}
// Getters and Setters
public String getUsername() {
return username;
}
public void setUsername(String username) {
this.username = username;
}
public String getEmail() {
return email;
}
public void setEmail(String email) {
this.email = email;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
@Override
public String toString() {
return "UserDTO{" +
"username='" + username + '\'' +
", email='" + email + '\'' +
", age=" + age +
'}';
}
}
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
关于**serialVersionUID
**字段:
- 建议为每个实现
Serializable
接口的类提供一个serialVersionUID
,以确保在反序列化时类的版本一致性。 serialVersionUID
是一个长整型值,可以自动生成或手动指定。
# serialVersionUID字段
serialVersionUID
是一个用于标识序列化类的版本号。如果你不显式地为一个实现了 Serializable
接口的类指定 serialVersionUID
,Java 会在编译时自动生成一个默认的 serialVersionUID
。然而,不显式指定 serialVersionUID
可能会导致一些潜在的问题和不便。以下是一些主要的影响和问题:
默认生成的 serialVersionUID
不稳定
Java 会根据类的结构(包括类名、方法签名、字段等)自动生成 serialVersionUID
。这意味着如果你对类的结构进行了任何修改(如添加、删除或修改字段、方法等),生成的 serialVersionUID
也会发生变化。这可能导致以下问题:
反序列化失败:如果你在不同的环境中(如开发、测试、生产)对类进行了修改,生成的
serialVersionUID
不同,尝试从旧版本的序列化数据中反序列化新版本的对象时,可能会抛出InvalidClassException
异常。兼容性问题:不同版本的类之间的兼容性问题,尤其是在分布式系统中,不同节点可能使用不同版本的类。
调试困难:默认生成的
serialVersionUID
使得调试反序列化错误更加困难。性能影响:虽然影响很小,但显式指定
serialVersionUID
可以减少不必要的计算开销。代码的一致性和可维护性:显式指定
serialVersionUID
使代码更加一致和可维护。
因此,为了确保序列化的稳定性和兼容性,建议在实现 Serializable
接口的类中显式指定 serialVersionUID
。
# 转为json字符串替代实现Serializable
当你将一个类转换为JSON字符串进行传输时,实际上已经完成了一种序列化过程。JSON字符串是一种文本格式,可以很容易地在网络上传输和存储。因此,这种情况下通常不需要实现 Serializable
接口,因为 Serializable
接口主要用于二进制序列化,而不是文本格式的序列化。
为什么不需要实现 Serializable
接口?
- JSON序列化:
- JSON序列化将对象转换为JSON字符串,这是一种文本格式,可以直接在网络上传输。
- 常用的JSON库(如Gson、Jackson、FastJSON)提供了方便的方法来将对象转换为JSON字符串和从JSON字符串转换回对象。
- 传输和存储:
- JSON字符串可以直接存储在文件中、数据库中,或者通过HTTP请求在网络上传输。
- 由于JSON是一种广泛支持的格式,可以很容易地在不同的编程语言和平台之间进行数据交换。