为什么需要序列化?
想象一下,你希望通过网络发送一个对象,比如一个 User 对象,网络传输只认识字节流,而 Java 对象是存在于内存中的复杂结构,直接发送一个对象是不可能的。

序列化 就是将 Java 对象转换成可以存储或传输的字节序列的过程。 反序列化 则是这个过程反过来,将字节序列重新恢复成原来的 Java 对象。
在 Socket 编程中,客户端和服务器之间传递的只能是字节流,任何需要通过网络传输的 Java 对象都必须先被序列化,然后在接收端进行反序列化。
Java 序列化的两种主要方式
在 Java 中,主要有两种实现序列化的方式:
- Java 原生序列化:Java 自带的标准机制,最简单直接,但也存在一些缺点。
- 第三方库序列化:如 JSON、Protobuf、Kryo 等,性能更好,跨语言支持更佳,是目前的主流选择。
Java 原生序列化详解
这是最基础的方法,了解它有助于理解序列化的本质。

如何实现?
要让一个类可以被 Java 原生序列化,这个类必须实现 java.io.Serializable 接口。
Serializable 接口是一个标记接口,它里面没有任何方法,它的作用只是告诉 JVM:“这个类的对象可以被序列化”。
示例代码:
import java.io.Serializable;
// 1. 实现 Serializable 接口
public class User implements Serializable {
// 2. 建议添加一个序列化版本 ID,用于版本控制
private static final long serialVersionUID = 1L;
private String name;
private int age;
// transient 关键字修饰的成员变量不会被序列化
private transient String password;
public User(String name, int age, String password) {
this.name = name;
this.age = age;
this.password = password;
}
@Override
public String toString() {
return "User{" +
"name='" + name + '\'' +
", age=" + age +
", password='" + (password == null ? "null" : "******") + '\'' + // 打印时隐藏密码
'}';
}
}
关键概念
-
serialVersionUID:
(图片来源网络,侵删)- 这是一个
long类型的静态 final 变量。 - 它的作用是版本控制,在序列化和反序列化时,JVM 会检查
serialVersionUID是否匹配。 - 如果不匹配,会抛出
InvalidClassException。 - 强烈建议:所有可序列化的类都显式地定义
serialVersionUID,如果不定义,JVM 会根据类的结构(字段、方法等)自动计算一个,这很危险,因为只要你对类做任何微小的修改(比如增加一个字段),自动计算的 ID 就会改变,导致旧数据无法反序列化。
- 这是一个
-
transient关键字:- 用
transient修饰的成员变量不会被序列化。 - 常用于修饰那些不希望被持久化或网络传输的敏感信息(如密码)或可以通过其他数据计算得出的字段(如缓存)。
- 用
在 Socket 编程中使用原生序列化
下面是一个完整的客户端-服务器示例,使用原生序列化传输 User 对象。
服务器端代码:
import java.io.*;
import java.net.ServerSocket;
import java.net.Socket;
public class Server {
public static void main(String[] args) throws IOException, ClassNotFoundException {
// 1. 创建 ServerSocket,监听指定端口
try (ServerSocket serverSocket = new ServerSocket(8888)) {
System.out.println("服务器启动,等待客户端连接...");
// 2. 监听并接受客户端的连接请求
try (Socket socket = serverSocket.accept()) {
System.out.println("客户端已连接: " + socket.getInetAddress());
// 3. 获取输入流,用于接收客户端发送的数据
InputStream is = socket.getInputStream();
ObjectInputStream ois = new ObjectInputStream(is);
// 4. 读取对象(反序列化)
User user = (User) ois.readObject();
System.out.println("服务器收到对象: " + user);
// 5. (可选)处理对象并发送响应
user.setAge(user.getAge() + 1);
OutputStream os = socket.getOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(os);
oos.writeObject(user);
System.out.println("服务器已返回修改后的对象: " + user);
}
}
}
}
客户端代码:
import java.io.*;
import java.net.Socket;
public class Client {
public static void main(String[] args) throws IOException, ClassNotFoundException {
// 1. 创建 Socket,连接到服务器的 IP 和端口
try (Socket socket = new Socket("localhost", 8888)) {
System.out.println("已连接到服务器");
// 2. 创建一个要发送的对象
User userToSend = new User("Alice", 30, "secret123");
// 3. 获取输出流,用于向服务器发送数据
OutputStream os = socket.getOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(os);
// 4. 发送对象(序列化)
oos.writeObject(userToSend);
System.out.println("客户端已发送对象: " + userToSend);
// 5. 获取输入流,接收服务器的响应
InputStream is = socket.getInputStream();
ObjectInputStream ois = new ObjectInputStream(is);
// 6. 读取服务器返回的对象(反序列化)
User receivedUser = (User) ois.readObject();
System.out.println("客户端收到服务器返回的对象: " + receivedUser);
}
}
}
Java 原生序列化的优缺点
-
优点:
- 简单易用:只需实现
Serializable接口,JVM 自动处理所有细节。 - 支持所有 Java 对象:理论上任何 Java 对象都可以被序列化。
- 简单易用:只需实现
-
缺点:
- 性能差:序列化后的字节流体积大,序列化/反序列化速度慢。
- 安全性问题:反序列化过程可能存在漏洞,如果攻击者精心构造恶意字节流,可能导致远程代码执行。
- 版本兼容性差:类的结构发生较大变化时,容易导致
InvalidClassException。 - 跨语言性差:这是 Java 独有的标准,其他语言(如 Python, C++)无法直接解析 Java 原生序列化后的字节流。
第三方序列化库(推荐实践)
由于原生序列化的诸多缺点,在实际生产项目中,通常会使用性能更好、更安全、更通用的第三方序列化库。
JSON (JavaScript Object Notation)
- 简介:轻量级的数据交换格式,易于人阅读和编写,也易于机器解析和生成,几乎所有编程语言都支持。
- Java 常用库:
Jackson,Gson,Fastjson(阿里巴巴)。 - 工作方式:将对象转换成 JSON 字符串(文本),在网络中传输,接收方收到字符串后,再解析成对象。
- 优点:
- 跨语言:通用性极强。
- 可读性好:序列化后的结果是文本,方便调试。
- 简单直观:结构清晰。
- 缺点:
- 性能相对 Protobuf 等二进制格式稍差。
- 体积较大:文本格式比二进制格式占用更多空间。
示例 (使用 Jackson):
-
添加 Maven 依赖:
<dependency> <groupId>com.fasterxml.jackson.core</groupId> <artifactId>jackson-databind</artifactId> <version>2.15.2</version> </dependency> -
代码改动:不再需要
Serializable,而是使用ObjectMapper。
// 序列化: 对象 -> JSON 字符串
User user = new User("Bob", 25, "password");
ObjectMapper mapper = new ObjectMapper();
String jsonString = mapper.writeValueAsString(user);
System.out.println(jsonString); // 输出: {"name":"Bob","age":25} (password字段被transient忽略)
// 反序列化: JSON 字符串 -> 对象
User deserializedUser = mapper.readValue(jsonString, User.class);
System.out.println(deserializedUser);
在 Socket 中,你只需将 jsonString 作为 byte[] 或 String 发送,接收方再解析即可。
Protocol Buffers (Protobuf)
- 简介:Google 开源的、语言中立、平台中立、可扩展的序列化结构数据的方法,它是一种二进制格式。
- 工作方式:
- 定义
.proto文件来描述数据结构。 - 使用 Protobuf 编译器生成特定语言的类(如 Java 类)。
- 使用生成的类进行序列化和反序列化。
- 定义
- 优点:
- 高性能:序列化速度快,体积小,非常适合网络传输和对性能要求高的场景。
- 向前/向后兼容:对
.proto文件进行修改(如增加字段)后,旧代码和新代码依然可以互相通信。 - 跨语言:支持 Java, Python, C++, Go 等多种语言。
- 缺点:
- 需要预编译:需要先定义
.proto文件并生成代码,增加了一步构建过程。 - 二进制格式不可读:调试时不如 JSON 方便。
- 需要预编译:需要先定义
Kryo
- 简介:一个快速、高效的 Java 序列化框架,在游戏和高性能计算领域非常流行。
- 优点:
- 速度极快:通常比原生序列化和 JSON 快很多。
- 体积小:生成的二进制数据非常紧凑。
- 缺点:
- 跨语言支持差:主要是为 Java 生态设计的。
- 需要注册类:为了达到最佳性能,通常需要提前注册所有需要序列化的类。
总结与选择
| 特性 | Java 原生序列化 | JSON | Protobuf | Kryo |
|---|---|---|---|---|
| 易用性 | ⭐⭐⭐⭐⭐ (最简单) | ⭐⭐⭐⭐ | ⭐⭐⭐ (需编译) | ⭐⭐⭐ (需注册) |
| 性能 | ⭐ | ⭐⭐ | ⭐⭐⭐⭐⭐ | ⭐⭐⭐⭐⭐ |
| 体积 | ⭐ | ⭐ | ⭐⭐⭐⭐⭐ | ⭐⭐⭐⭐⭐ |
| 跨语言 | ❌ | ✅ (极佳) | ✅ (好) | ❌ (仅Java) |
| 安全性 | ⚠️ (有风险) | ✅ | ✅ | ✅ |
| 适用场景 | 简单的Demo、内部Java应用、对性能要求不高的场景 | Web API、配置文件、需要跨语言交互的场景 | 微服务、RPC框架、移动端、对性能和体积要求极高的场景 | 游戏、大数据处理、Java内部高性能数据交换 |
如何选择?
- 学习/快速原型:如果只是写个小例子或学习 Socket,Java 原生序列化 足够,因为它最简单。
- Web 开发/对外 API:如果你的服务需要被其他语言(如 JavaScript, Python)调用,JSON 是不二之选。
- 高性能/微服务/移动端:如果你的应用对网络带宽和 CPU 消耗非常敏感(如高频交易、大型游戏、分布式系统),Protobuf 或 Kryo 是更好的选择,Protobuf 因其跨语言优势更通用,Kryo 在纯 Java 环境下可能更快。
对于任何严肃的 Java 项目,都强烈建议放弃 Java 原生序列化,拥抱更现代、更高效的第三方序列化方案。
