两者核心概念对比
什么是 Java 原生序列化?
Java 原生序列化是 Java 语言自带的机制,它允许将任何实现了 java.io.Serializable 接口的对象转换为字节流,以便可以存储到文件、数据库中,或者通过网络传输,反序列化则是将字节流恢复成原始对象。

- 核心思想:将对象“原封不动”地转换成字节流,这个过程是“深度”的,会递归地序列化对象及其所有引用的对象。
- 实现方式:实现
Serializable接口,JVM 会自动处理序列化逻辑,也可以实现Externalizable接口来获得完全的控制权。
什么是 Protocol Buffers (Protobuf)?
Protobuf 是 Google 公司开发的一种语言无关、平台无关的序列化协议,它类似于 XML、JSON,但更小、更快、更简单,你首先需要定义 .proto 文件来描述你的数据结构,然后使用 Protobuf 编译器生成特定语言的类(如 Java 类)。
- 核心思想:基于一个结构化的定义文件(
.proto),生成高效的序列化和反序列化代码,它将数据序列化为二进制格式,而不是文本格式。 - 实现方式:
- 编写
.proto文件。 - 使用
protoc编译器生成目标语言(如 Java)的源代码。 - 在 Java 代码中使用生成的类来构建对象和序列化/反序列化。
- 编写
核心差异对比 (一张图看懂)
| 特性 | Java 原生序列化 | Protocol Buffers (Protobuf) |
|---|---|---|
| 性能 | 慢 | 极快 (比 Java 序列化快 20-100 倍) |
| 数据大小 | 大 (包含大量元数据) | 极小 (二进制格式,非常紧凑) |
| 可读性 | 差 (二进制格式,不可读) | 差 (二进制格式,不可读,但有文本格式 TextFormat) |
| 跨语言/平台 | 差 (主要限于 JVM) | 极佳 (支持 Java, Python, Go, C++, C# 等 10+ 种语言) |
| 向前/向后兼容性 | 差 (修改类结构极易破坏兼容性) | 优秀 (通过字段编号管理,增删字段非常安全) |
| 使用门槛 | 低 (只需实现接口,无需额外工具) | 高 (需要定义 .proto 文件,使用编译器) |
| 安全性 | 差 (可能执行恶意代码,存在安全漏洞) | 高 (反序列化过程是安全的,不会执行任意代码) |
| 版本控制 | 困难 (依赖 serialVersionUID) |
简单 (通过 .proto 文件版本管理) |
| 灵活性 | 高 (可以序列化几乎任何对象) | 较低 (只能序列化在 .proto 文件中定义的数据结构) |
详细解释关键差异
a. 性能与数据大小
这是 Protobuf 最大的优势。
- Java 序列化:在序列化时,除了数据本身,还会写入大量元数据,比如类的描述信息、字段名、类型签名等,这使得数据包非常臃肿。
- Protobuf:序列化后是紧凑的二进制格式,它不存储字段名,而是存储每个字段的数字编号。
name= "Alice"会被编码成类似[1, 5, 'A', 'l', 'i', 'c', 'e']的形式,1是name字段的编号,5是字符串的长度,这使得数据量非常小,序列化和反序列化的速度也极快。
b. 跨语言/平台
- Java 序列化:是 JVM 内部的协议,一个在 Java 中序列化的对象,无法被 Python 或 Go 直接反序列化,除非你用 Java 写一个网关服务来转换数据。
- Protobuf:天生就是为了跨语言设计的,你可以用 Java 定义数据结构,然后用 Python 序列化,再用 Go 反序列化,完全没有障碍。
c. 向后/向前兼容性
这是 Protobuf 在分布式系统中的“杀手锏”。
-
Java 序列化:非常脆弱,如果你在
Person类中增加了一个新字段age,那么旧的、没有age字段的序列化字节流在反序列化时就会抛出异常,虽然有serialVersionUID可以控制版本,但它只解决类的标识问题,无法解决字段变更的问题。
(图片来源网络,侵删) -
Protobuf:非常健壮,规则是:
- 不要重用已删除字段的编号。
- 不要更改任何现有字段的编号。
- 可以安全地添加新字段(旧代码会忽略新字段,新代码可以读取旧数据)。
- 可以安全地标记字段为
deprecated(旧代码会忽略它,新代码最终会停止使用它)。
这个特性使得服务升级变得非常平滑,无需一次性更新所有服务。
如何在 Java 中使用 Protobuf (实战演练)
步骤 1: 定义 .proto 文件
创建一个文件 person.proto。
// syntax = "proto3"; // 指定使用 proto3 语法
package com.example; // Java 包名
// 定义一个消息,类似于 Java 的类
message Person {
int32 id = 1; // 字段编号 1
string name = 2; // 字段编号 2
string email = 3; // 字段编号 3
// 嵌套消息
message PhoneNumber {
string number = 1;
PhoneType type = 2;
}
repeated PhoneNumber phones = 4; // repeated 表示数组/列表
// 枚举
enum PhoneType {
MOBILE = 0;
HOME = 1;
WORK = 2;
}
}
步骤 2: 添加 Maven 依赖
在你的 pom.xml 中添加 Protobuf Maven 插件和运行时依赖。

<properties>
<protobuf.version>3.25.1</protobuf.version>
<maven.compiler.plugin.version>3.11.0</maven.compiler.plugin.version>
</properties>
<dependencies>
<!-- Protobuf Java 运行时依赖 -->
<dependency>
<groupId>com.google.protobuf</groupId>
<artifactId>protobuf-java</artifactId>
<version>${protobuf.version}</version>
</dependency>
</dependencies>
<build>
<plugins>
<!-- Protobuf Maven 插件 -->
<plugin>
<groupId>org.xolstice.maven.plugins</groupId>
<artifactId>protobuf-maven-plugin</artifactId>
<version>0.6.1</version>
<configuration>
<protocArtifact>com.google.protobuf:protoc:${protobuf.version}:exe:${os.detected.classifier}</protocArtifact>
<pluginId>grpc-java</pluginId>
<protoSourceRoot>${basedir}/src/main/proto</protoSourceRoot>
</configuration>
<executions>
<execution>
<goals>
<goal>compile</goal>
</goals>
</execution>
</executions>
</plugin>
<!-- 为了编译生成的 Java 文件,需要添加此插件 -->
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>${maven.compiler.plugin.version}</version>
<configuration>
<source>8</source>
<target>8</target>
</configuration>
</plugin>
</plugins>
</build>
步骤 3: 编译 .proto 文件
在项目根目录下执行 Maven 命令:
mvn clean compile
执行后,插件会自动调用 protoc 编译器,并在 target/generated-sources/protobuf/java/com/example/ 目录下生成 Person.java、PersonOrBuilder.java 等文件,你需要将这些生成的源文件添加到项目的源码路径中(现代 IDE 如 IntelliJ IDEA 通常会自动识别)。
步骤 4: 在 Java 代码中使用生成的类
你可以像使用普通 Java 类一样使用 Person 了。
import com.example.Person;
import java.io.FileOutputStream;
import java.io.FileInputStream;
import java.io.IOException;
public class ProtobufDemo {
public static void main(String[] args) {
// 1. 创建一个 Person 对象
Person person = Person.newBuilder()
.setId(123)
.setName("Alice")
.setEmail("alice@example.com")
.addPhones(
Person.PhoneNumber.newBuilder()
.setNumber("138-8888-8888")
.setType(Person.PhoneType.MOBILE)
.build()
)
.build();
System.out.println("Created Person: " + person);
System.out.println("Person size in bytes: " + person.getSerializedSize());
// 2. 序列化到文件
String filename = "person.data";
try (FileOutputStream output = new FileOutputStream(filename)) {
person.writeTo(output);
System.out.println("Person has been serialized to " + filename);
} catch (IOException e) {
e.printStackTrace();
}
// 3. 从文件反序列化
try (FileInputStream input = new FileInputStream(filename)) {
Person parsedPerson = Person.parseFrom(input);
System.out.println("\nParsed Person from file:");
System.out.println("ID: " + parsedPerson.getId());
System.out.println("Name: " + parsedPerson.getName());
System.out.println("Email: " + parsedPerson.getEmail());
System.out.println("Phone: " + parsedPerson.getPhones(0).getNumber() + " (" + parsedPerson.getPhones(0).getType() + ")");
} catch (IOException e) {
e.printStackTrace();
}
}
}
总结与选型建议
| 场景 | 推荐方案 | 理由 |
|---|---|---|
| 微服务间通信 | Protobuf (或 gRPC/Thrift) | 首选,性能高、数据小、跨语言、强兼容性,是分布式系统的标配。 |
| RPC 框架 | Protobuf (gRPC) | gRPC 使用 Protobuf 作为接口定义语言和序列化格式,提供了高性能、强类型的 RPC 体验。 |
| 持久化存储 | Protobuf | 当存储空间和读写性能是关键时,Protobuf 比 JSON/XML 更优。 |
| 简单的配置文件 | JSON/YAML | 人类可读性是首要考虑因素,JSON/YAML 更直观。 |
| 简单的 JVM 内部对象传递 | Java 序列化 | 代码简单,无需额外工具,但仅限 JVM 内部,不推荐用于网络或持久化。 |
| 遗留系统维护 | Java 序列化 | 如果系统已经深度依赖 Java 序列化,重构成本可能很高,只能继续维护。 |
在现代 Java 开发中,除非是极特殊、简单的场景,否则都应该优先选择 Protocol Buffers 而不是 Java 原生序列化,Protobuf 在性能、兼容性和跨平台性方面的巨大优势,使其成为构建高性能、可扩展服务的基石,Java 原生序列化更多地存在于历史遗留代码中,或者是一些仅在 JVM 内部进行简单对象传递的场景。
