京东二面:说说Java序列化和反序列化

这是有小伙伴最近遇到的面试题。一起来看看。

一 什么是序列化和反序列化

这是两个大家经常会接触的概念~

序列化是指将对象的状态信息转换为可以存储或传输的形式的过程。在Java中,这通常意味着将对象转换为字节序列。

反序列化则是序列化的逆过程,它将字节序列恢复为对象。

一个简单的例子,对象转为 JSON 字符串就是序列化,因为变为 JSON 字符串之后就可以传输了;JSON 字符串转为对象则是反序列化。

二 序列化与反序列化的使用场景

我们日常开发中,序列化和反序列化其实还是挺常见的,只不过有时候我们忘记了自己所做的事情其实就是序列化和反序列化。

举几个常见的场景:

  1. 网络传输:在分布式系统中,对象需要在网络上传输时,需要将对象序列化后发送,接收方再进行反序列化。
  2. 数据存储:将对象状态保存到文件或数据库中,以便后续恢复。
  3. 远程方法调用(RMI):在 Java 的 RMI 中,对象需要在客户端和服务器之间传递。
  4. 对象克隆:通过序列化和反序列化实现对象的深拷贝。

类似的场景其实很多。

不过就日常开发而言,可能大家在从 Redis 中存取对象、Dubbo 远程调用,这些场景可能会明确感知到序列化这件事,其他场景可能感受就不是特别明显。

三 Java 中实现序列化与反序列化

3.1 实现 Serializable 接口

假设有一个Student类,需要将其对象序列化到文件中。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
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();
}
}
}

在这个案例中,Student 类通过实现 Serializable 接口来标记其可序列化。然后,使用 ObjectOutputStream 将 Student 对象写入到文件中。

3.2 实现 Externalizable 接口

与 Serializable 接口类似,但 Externalizable 接口提供了更灵活的序列化控制。

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
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方法
}
}

在这个案例中,Employee 类通过实现 Externalizable 接口来提供自定义的序列化和反序列化逻辑。

相比于 Serializable,Externalizable 的灵活性主要体现在四点:

  1. Serializable 接口的序列化过程是由 JVM 自动完成的,不允许开发者对序列化过程进行自定义。Externalizable 接口要求开发者实现 writeExternal() 和 readExternal() 两个方法,从而完全控制序列化过程。这意味着开发者可以决定哪些字段需要序列化,哪些不需要,以及如何序列化这些字段。
  2. Serializable 的自动序列化过程虽然方便,但可能不是最高效的。Externalizable 允许开发者自定义序列化过程,因此可以针对特定需求进行优化。例如,可以只序列化必要的字段,或者采用更高效的数据结构来存储序列化数据,从而提高性能。
  3. Serializable 在默认情况下会序列化对象的所有非 transient 字段。如果对象的类结构发生变化如添加或删除字段,则可能会影响序列化和反序列化的兼容性。而对于 Externalizable 接口,开发者可以精确控制哪些字段被序列化,从而更容易地管理版本兼容性问题,甚至还可以在 writeExternal() 和 readExternal() 方法中添加逻辑来处理不同版本的序列化数据。
  4. 由于 Serializable 的序列化过程是自动的,因此可能会无意中序列化敏感信息(如密码、密钥等),此外,恶意用户还可能通过修改序列化数据来攻击系统。而 Externalizable 接口允许开发者明确控制哪些信息被序列化,从而可以减少敏感信息被泄露的风险,开发者甚至还可以在序列化过程中添加额外的安全措施(如加密、签名等)来提高系统的安全性。

3.3 使用 JSON 序列化库(如 Jackson、Gson)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
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();
}
}
}

在这个案例中,使用 Jackson 库将 User 对象序列化为 JSON 字符串,并反序列化为 User 对象。

3.4 使用 XML 序列化库(如JAXB、XStream)

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
import javax.xml.bind.JAXBContext;  
import javax.xml.bind.Marshaller;
import javax.xml.bind.annotation.XmlRootElement;

@XmlRootElement
public class Address {
private String street;
private String city;

// 构造函数、getter和setter省略

public static void main(String[] args) {
try {
Address address = new Address("123 Main St", "Anytown");
JAXBContext jaxbContext = JAXBContext.newInstance(Address.class);
Marshaller marshaller = jaxbContext.createMarshaller();
marshaller.setProperty(Marshaller.JAXB_FORMATTED_OUTPUT, true);
marshaller.marshal(address, System.out); // 输出到控制台

// 反序列化逻辑通常涉及解析XML字符串或文件
} catch (Exception e) {
e.printStackTrace();
}
}
}

在这个案例中,Address 类通过 @XmlRootElement 注解标记为可 XML 序列化。使用 JAXB 库将其序列化为 XML 格式并输出到控制台。

3.5 使用二进制序列化库(如 protobuf、Avro)

由于 Protobuf 的使用涉及 .proto 文件的定义和编译,这里仅简要说明。首先定义一个 .proto文件,然后使用 Protobuf 编译器生成 Java 代码,最后使用生成的代码进行序列化和反序列化。

这个松哥之前在 gRPC 教程中详细介绍过,这里不多说了,公号后台回复 gRPC。

四 static 和 transient

static 和 transient 是序列化时两个比较特殊的字段。

  • staticstatic 字段是类级别的,不属于对象实例,因此在序列化时不会被包含。
  • transienttransient 关键字修饰的字段在序列化过程中会被忽略,不会被序列化。

五 小结

以上就是松哥和大家介绍的 Java 中的序列化问题。

另外有几个点大家在序列化时候需要注意:

  1. 版本控制:通过 serialVersionUID 来确保序列化的兼容性。
  2. 性能考虑:序列化和反序列化是资源密集型操作,应考虑性能影响。
  3. 安全性:确保序列化数据的安全性,避免序列化过程中的数据泄露。
  4. 数据一致性:在反序列化时,确保数据的一致性和完整性。