1. 概述

序列化是将对象转换为字节流的过程。然后可以将该对象保存到数据库或通过网络传输。相反的操作,即从一系列字节中提取对象,是反序列化。它们的主要目的是保存对象的状态,以便我们可以在需要时重新创建它。

在本教程中, 我们将探索 Java 对象的不同序列化方法

首先,我们将讨论用于序列化的 Java 本机 API。接下来,我们将探索支持 JSON 和 YAML 格式的库来执行相同的操作。最后,我们将看一些跨语言协议。

2. 示例实体类

让我们首先介绍一个我们将在本教程中使用的简单实体:

public class User {
    private int id;
    private String name;
    
    //getters and setters
}

在接下来的部分中,我们将介绍最广泛使用的序列化协议。通过示例,我们将了解它们的基本用法。

3. Java 的原生序列化

Java中的序列化有助于实现多个系统之间有效、迅速的通信。 Java 指定了一种默认的对象序列化方式。 Java 类可以覆盖此默认序列化并定义自己的序列化对象的方式。

Java 原生序列化的优点是:

  • 这是一个简单但可扩展的机制
  • 它以序列化形式维护对象类型和安全属性
  • 可扩展以支持远程对象所需的编组和解组
  • 这是一个本机 Java 解决方案,因此不需要任何外部库

3.1.默认机制

根据Java 对象序列化规范,我们可以使用 ObjectOutputStream 类中的 writeObject() 方法来序列化对象。另一方面,我们可以使用 ObjectInputStream 类的 readObject() 方法来执行反序列化。

我们将 使用 User 类来说明基本过程。

首先,我们的类需要 实现 Serialized 接口

public class User implements Serializable {
    //fields and methods
}

接下来,我们需要 添加 serialVersionU**ID 属性

private static final long serialVersionUID = 1L;

现在,让我们创建一个 User 对象:

User user = new User();
user.setId(1);
user.setName("Mark");

我们需要提供一个文件路径来保存我们的数据:

String filePath = "src/test/resources/protocols/user.txt";

现在,是时候将我们的 User 对象序列化到文件中了:

FileOutputStream fileOutputStream = new FileOutputStream(filePath);
ObjectOutputStream objectOutputStream = new ObjectOutputStream(fileOutputStream);
objectOutputStream.writeObject(user);

在这里,我们使用 ObjectOutputStreamUser 对象的状态保存到 “user.txt” 文件中。

另一方面,我们可以从同一文件中读取 User 对象并将其反序列化:

FileInputStream fileInputStream = new FileInputStream(filePath);
ObjectInputStream objectInputStream = new ObjectInputStream(fileInputStream);
User deserializedUser = (User) objectInputStream.readObject();

最后,我们可以测试加载对象的状态:

assertEquals(1, deserializedUser.getId());
assertEquals("Mark", deserializedUser.getName());

这是序列化 Java 对象的默认方式。在下一节中,我们将看到执行相同操作的自定义方法。

3.2.使用 可外部化 接口的自定义序列化

当尝试序列化具有某些不可序列化属性的对象时,自定义序列化特别有用。这可以通过实现 Externalized 接口来完成,该接口有两个方法:

public void writeExternal(ObjectOutput out) throws IOException;

public void readExternal(ObjectInput in) throws IOException, ClassNotFoundException;

我们可以在要序列化的类中实现这两个方法。详细的示例可以在我们关于 外部化 接口的文章中找到。

3.3. Java 序列化注意事项

Java 中的本机序列化有一些注意事项:

  • 只有标记 为可序列化的 对象才能被持久化。 Object 类没有实现 Serialized, 因此并不是所有Java中的对象都可以自动持久化
  • 当一个类实现 Serialized 接口时,它的所有子类也都是可序列化的。但是, 当一个对象引用另一个对象时,这些对象必须单独实现 Serialized 接口,否则会抛出 NotSerializedException
  • 如果我们想控制版本控制,我们需要提供 serialVersionUID 属性。 该属性用于验证保存和加载的对象是否兼容。因此,我们需要确保它始终相同,否则将抛出 InvalidClassException
  • Java 序列化大量使用 I/O 流。我们需要在读取或写入操作后立即关闭流,因为 如果我们忘记关闭流,我们最终会导致资源泄漏 。为了防止此类资源泄漏,我们可以使用 try-with-resources 习惯用法

4.Gson库

Google 的Gson是一个 Java 库,用于将 Java 对象与 JSON 表示进行序列化和反序列化。

Gson 是一个托管在GitHub上的开源项目。一般来说,它提供 toJson()fromJson() 方法来将 Java 对象转换为 JSON,反之亦然。

4.1. Maven依赖

让我们添加Gson 库的依赖项:

<dependency>
    <groupId>com.google.code.gson</groupId>
    <artifactId>gson</artifactId>
    <version>2.8.7</version>
</dependency>

4.2. Gson序列化

首先,我们创建一个 User 对象:

User user = new User();
user.setId(1);
user.setName("Mark");

接下来,我们需要提供一个文件路径来保存 JSON 数据:

String filePath = "src/test/resources/protocols/gson_user.json";

现在,让我们使用 Gson 类中的 toJson() 方法将 User 对象序列化到“ gson_user.json” 文件中:

Writer writer = new FileWriter(filePath);
Gson gson = new GsonBuilder().setPrettyPrinting().create();
gson.toJson(user, writer);

4.3. Gson反序列化

我们可以使用 Gson 类中的 fromJson() 方法来反序列化 JSON 数据。

让我们读取 JSON 文件并将数据反序列化为 User 对象:

Gson gson = new GsonBuilder().setPrettyPrinting().create();
User deserializedUser = gson.fromJson(new FileReader(filePath), User.class);

最后,我们可以测试反序列化后的数据:

assertEquals(1, deserializedUser.getId());
assertEquals("Mark", deserializedUser.getName());

4.4. Gson特点

Gson 有许多重要的特性,包括:

  • 它可以处理集合、泛型类型和嵌套类
  • 使用Gson,我们还可以编写自定义序列化器和/或反序列化器,以便我们可以控制整个过程
  • 最重要的是, 它允许反序列化无法访问源代码的类的实例
  • 此外,我们可以使用版本控制功能,以防我们的类文件在不同版本中被修改。 我们可以在新添加的字段上使用 @Since 注释,然后我们可以使用 GsonBuilder 中的 setVersion() 方法

有关更多示例,请查看我们的Gson 序列化Gson 反序列化食谱。

在本节中,我们使用 Gson API 序列化 JSON 格式的数据。在下一节中,我们将使用 Jackson API 来执行相同的操作。

5.杰克逊API

Jackson也被称为“Java JSON 库”或“Java 的最佳 JSON 解析器”。它提供了多种处理 JSON 数据的方法。

要总体了解 Jackson 库,我们的Jackson 教程是一个很好的起点。

5.1. Maven依赖

让我们添加Jackson 库的依赖项:

<dependency>
    <groupId>com.fasterxml.jackson.core</groupId>
    <artifactId>jackson-core</artifactId>
    <version>2.12.4</version>
</dependency>
<dependency>
    <groupId>com.fasterxml.jackson.core</groupId>
    <artifactId>jackson-annotations</artifactId>
    <version>2.12.4</version>
</dependency>
<dependency>
    <groupId>com.fasterxml.jackson.core</groupId>
    <artifactId>jackson-databind</artifactId>
     <version>2.12.4</version>
</dependency>

5.2. Java 对象到 JSON

我们可以使用 ObjectMapper 类的 writeValue() 方法将任何 Java 对象序列化为 JSON 输出。

让我们从创建一个 User 对象开始:

User user = new User();
user.setId(1);
user.setName("Mark Jonson");

之后,让我们提供一个文件路径来存储 JSON 数据:

String filePath = "src/test/resources/protocols/jackson_user.json";

现在,我们可以使用 ObjectMapper 类将 User 对象存储到 JSON 文件中:

File file = new File(filePath);
ObjectMapper mapper = new ObjectMapper();
mapper.writeValue(file, user);

此代码会将我们的数据写入 “jackson_user.json” 文件。

5.3. JSON 到 Java 对象

ObjectMapper 的简单 readValue() 方法是一个很好的入口点。 我们可以使用它将 JSON 内容反序列化为 Java 对象。

让我们从 JSON 文件中读取 User 对象:

User deserializedUser = mapper.readValue(new File(filePath), User.class);

我们总是可以测试加载的数据:

assertEquals(1, deserializedUser.getId());
assertEquals("Mark Jonson", deserializedUser.getName());

5.4.杰克逊特色

  • Jackson 是一个可靠且成熟的 Java JSON 序列化库
  • ObjectMapper 类是序列化过程的入口点,提供了一种解析和生成 JSON 对象的简单方法,具有很大的灵活性
  • Jackson 库的最大优势之一是高度可定制的序列化反序列化过程

到目前为止,我们看到的是 JSON 格式的数据序列化。在下一节中,我们将探索使用 YAML 的序列化。

6.YAML

YAML代表“YAML 不是标记语言”。它是一种人类可读的数据序列化语言。 我们可以将 YAML 用于配置文件,以及我们想要存储或传输数据的应用程序。

在上一节中,我们看到了 Jackson API 处理 JSON 文件。我们还可以使用 Jackson API 来处理 YAML 文件。详细的示例可以在我们关于使用 Jackson 解析 YAML 的文章中找到。

现在,让我们看看其他库。

6.1. YAML 豆

YAML Beans可以轻松地将 Java 对象图与 YAML 进行序列化和反序列化。

YamlWriter 类用于将 Java 对象序列化为 YAML。 write() 方法通过识别公共字段和 bean 的 getter 方法自动处理此问题。

相反,我们可以使用 YamlReader 类将 YAML 反序列化为 Java 对象。 read() 方法读取 YAML 文档并将其反序列化为所需的对象。

首先,让我们添加YAML Beans的依赖项:

<dependency>
    <groupId>com.esotericsoftware.yamlbeans</groupId>
    <artifactId>yamlbeans</artifactId>
    <version>1.15</version>
</dependency>

现在。让我们创建一个 User 对象的映射:

private Map<String, User> populateUserMap() {
    User user1 = new User();
    user1.setId(1);
    user1.setName("Mark Jonson");
    //.. more user objects
    
    Map<String, User> users = new LinkedHashMap<>();
    users.put("User1", user1);
    // add more user objects to map
    
    return users;
}

之后,我们需要提供一个文件路径来存储我们的数据:

String filePath = "src/test/resources/protocols/yamlbeans_users.yaml";

现在,我们可以使用 YamlWriter 类将映射序列化为 YAML 文件:

YamlWriter writer = new YamlWriter(new FileWriter(filePath));
writer.write(populateUserMap());
writer.close();

另一方面,我们可以使用 YamlReader 类来反序列化地图:

YamlReader reader = new YamlReader(new FileReader(filePath));
Object object = reader.read();
assertTrue(object instanceof Map); 

最后,我们可以测试加载的地图:

Map<String, User> deserializedUsers = (Map<String, User>) object;
assertEquals(4, deserializedUsers.size());
assertEquals("Mark Jonson", (deserializedUsers.get("User1").getName()));
assertEquals(1, (deserializedUsers.get("User1").getId()));

6.2.蛇YAML

SnakeYAML提供了一个高级 API 将 Java 对象序列化为 YAML 文档,反之亦然。最新版本 1.2 可与 JDK 1.8 或更高版本的 Java 一起使用。它可以解析 Java 结构,例如 StringListMap

SnakeYAML 的入口点是 Yaml 类,其中包含多个有助于序列化和反序列化的方法。

要将 YAML 输入反序列化为 Java 对象,我们可以使用 load() 方法加载单个文档,并使用 loadAll() 方法加载多个文档。这些方法接受 InputStream 以及 String 对象。

另一方面,我们可以使用 dump() 方法将 Java 对象序列化为 YAML 文档。

详细的示例可以在我们关于使用 SnakeYAML 解析 YAML 的文章中找到。

* 当然,SnakeYAML 可以很好地与 Java Map 配合使用,但是,它也可以与自定义 Java 对象配合使用。 *

在本节中,我们看到了将数据序列化为 YAML 格式的不同库。在接下来的部分中,我们将讨论跨平台协议。

7.阿帕奇节俭

Apache Thrift最初由 Facebook 开发,目前由 Apache 维护。

使用Thrift的最大好处是 支持跨语言序列化,开销较低 。此外,许多序列化框架仅支持一种序列化格式,但是 Apache Thrift 允许我们从多种序列化格式中进行选择。

7.1.节俭功能

Thrift 提供可插入的序列化器,称为协议。这些协议提供了使用多种序列化格式中的任何一种进行数据交换的灵活性。 支持的协议的一些示例包括:

  • TBinaryProtocol 使用二进制格式,因此比文本协议处理速度更快
  • TCompactProtocol 是一种更紧凑的二进制格式,因此处理起来也更高效
  • TJSONProtocol 使用 JSON 来编码数据

Thrift 还支持容器类型的序列化——列表、集合和映射。

7.2. Maven依赖

要在我们的应用程序中使用 Apache Thrift 框架,让我们添加Thrift 库

<dependency>
    <groupId>org.apache.thrift</groupId>
    <artifactId>libthrift</artifactId>
    <version>0.14.2</version>
</dependency>

7.3. Thrift 数据序列化

Apache Thrift 协议和传输旨在作为分层堆栈一起工作。协议将数据序列化为字节流,而传输则读取和写入字节。

如前所述,Thrift 提供了许多协议。我们将使用二进制协议来说明 Thrift 序列化。

首先,我们需要一个 User 对象:

User user = new User();
user.setId(2);
user.setName("Greg");

下一步是创建二进制协议:

TMemoryBuffer trans = new TMemoryBuffer(4096);
TProtocol proto = new TBinaryProtocol(trans);

现在,让我们序列化我们的数据*.* 我们可以使用 写入 API 来执行此操作:

proto.writeI32(user.getId());
proto.writeString(user.getName());

7.4. Thrift 数据反序列化

让我们使用 读取 API 来反序列化数据:

int userId = proto.readI32();
String userName = proto.readString();

最后,我们可以测试加载的数据:

assertEquals(2, userId);
assertEquals("Greg", userName);

更多示例可以在我们关于Apache Thrift的文章中找到。

8. 谷歌协议缓冲区

我们将在本教程中介绍的最后一种方法是Google Protocol Buffers (protobuf)。它是一种众所周知的二进制数据格式。

8.1. Protocol Buffer 的优点

协议缓冲区具有多种优势,包括:

  • 它与语言和平台无关
  • 它是一种二进制传输格式,意味着数据以二进制形式传输。这提高了传输速度,因为它占用的空间和带宽更少
  • 支持向后和向前兼容,以便新版本可以读取旧数据,反之亦然

8.2. Maven依赖

让我们首先添加Google protocol buffer 库的依赖项:

<dependency>
    <groupId>com.google.protobuf</groupId>
    <artifactId>protobuf-java</artifactId>
    <version>3.17.3</version>
</dependency>

8.3.定义协议

消除依赖关系后,我们现在可以定义消息格式:

syntax = "proto3";
package protobuf;
option java_package = "com.baeldung.serialization.protocols";
option java_outer_classname = "UserProtos";
message User {
    int32 id = 1;
    string name = 2;
}

这是一个 User 类型的简单消息的协议,它有两个字段 - idname ,分别为 整数字符串 类型。请注意,我们将其保存为 “user.proto” 文件。

8.4.从 Protobuf 文件生成 Java 代码

一旦我们有了 protobuf 文件,我们就可以使用 protoc 编译器从它生成代码:

protoc -I=. --java_out=. user.proto

结果,该命令将生成 UserProtos.java 文件。

之后,我们可以创建 UserProtos 类的实例:

UserProtos.User user = UserProtos.User.newBuilder().setId(1234).setName("John Doe").build();

8.5。序列化和反序列化 Protobuf

首先,我们需要提供一个文件路径来存储我们的数据:

String filePath = "src/test/resources/protocols/usersproto";

现在,让我们将数据保存到文件中。我们可以使用 UserProtos 类中的 writeTo() 方法——我们从 protobuf 文件生成的类:

FileOutputStream fos = new FileOutputStream(filePath);
user.writeTo(fos);

执行此代码后,我们的对象将被序列化为二进制格式并保存到“ usersproto ”文件中。

相反,我们可以使用 mergeFrom() 方法从文件加载该数据并将其反序列化回 User 对象:

UserProtos.User deserializedUser = UserProtos.User.newBuilder().mergeFrom(new FileInputStream(filePath)).build();

最后,我们可以测试加载的数据:

assertEquals(1234, deserializedUser.getId());
assertEquals("John Doe", deserializedUser.getName());

9. 总结

在本教程中,我们探讨了一些广泛使用的 Java 对象序列化协议。应用程序数据序列化格式的选择取决于多种因素,例如数据复杂性、人类可读性的需求和速度。

Java 支持易于使用的内置序列化。

由于可读性和无模式,JSON 更可取。因此,Gson 和 Jackson 都是序列化 JSON 数据的不错选择。 它们使用简单且有据可查。 对于编辑数据,YAML 非常适合。

另一方面,二进制格式比文本格式更快。 当速度对我们的应用程序很重要时,Apache Thrift 和 Google Protocol Buffers 是序列化数据的绝佳选择。 两者都比 XML 或 JSON 格式更紧凑、更快。

总而言之,便利性和性能之间通常需要权衡,序列化也不例外。当然,还有许多其他格式可用于数据序列化

与往常一样,完整的示例代码位于 GitHub 上