1. 为什么需要序列化
想象你的对象就像你去度假时要带的物品。序列化就是把行李箱里的所有内容“打包”进一个特殊容器,方便托运或邮寄。相应地,反序列化就是将这个容器“拆包”,把物品以原样取回。
本质上,序列化把对象转换为字节流,可以保存到文件、通过网络传输,或者仅在内存中存放。反序列化则相反:从该字节流中还原对象。打个比方,序列化就像把对象“冻结”,以后再“解冻”回来,得到同样状态的对象。
在程序多次启动之间保存对象状态
最常见的场景之一是保存程序状态。例如,你有用户列表、游戏结果或应用设置。这些数据用对象来表示最方便。为了避免在多次运行之间丢失数据,可以把它们序列化到文件,下次启动时再反序列化回来。
一个很好理解的例子是游戏存档。当玩家通关一个关卡时,进度被“冻结”并通过序列化写入文件。第二天打开游戏时,进度被“解冻”:文件中的数据再次变回对象,玩家就能从上次停下的位置继续。
我们来做一个简单的存档:
import java.io.*;
// Player 类必须实现 Serializable
class Player implements Serializable {
String name;
int score;
Player(String name, int score) {
this.name = name;
this.score = score;
}
}
public class GameSaveExample {
public static void main(String[] args) throws Exception {
// 创建一个玩家对象
Player player = new Player("Ihor", 1500);
// --- 保存(序列化) ---
try (ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("save.dat"))) {
out.writeObject(player);
System.out.println("进度已保存!");
}
// --- 加载(反序列化) ---
try (ObjectInputStream in = new ObjectInputStream(new FileInputStream("save.dat"))) {
Player loaded = (Player) in.readObject();
System.out.println("进度已加载: " + loaded.name + ",积分为 " + loaded.score);
}
}
}
请注意:为了让这段代码正常工作,类 Player 必须实现接口 Serializable。关于它的详细内容将在下一讲介绍!
- Player —— 一个带有姓名和分数字段的普通类,并实现了接口 Serializable(implements Serializable)。
- ObjectOutputStream 把对象写入文件 "save.dat"。
- ObjectInputStream 再把同一个对象读回来。
- 最终我们得到真正的存档:下次启动时,程序会以相同的状态加载玩家对象。
在网络和不同 JVM 之间传输对象
在分布式系统中,经常需要在不同程序甚至不同机器之间传递对象。例如,有一个客户端和一个服务器需要交换消息。序列化允许在一端把对象“打包”、通过网络发送,并在另一端“拆包”。
示例:客户端向服务器发送一个订单对象(Order),服务器接收、反序列化并处理。
Java 技术中的使用
- RMI(Remote Method Invocation):允许调用远程对象的方法——序列化用于传递参数和返回值。
- HTTP 会话:在 Servlet 中,会话里的对象在容器重启时会被序列化。
- JMS(Java Message Service):组件之间的消息可以被序列化。
- 缓存:对象可以被序列化后存入缓存(磁盘或分布式存储)。
缓存与可移植性
如果你想快速保存中间结果(例如用于缓存),序列化是很好的工具。你把对象序列化后保存到磁盘或内存,随后无需重复计算即可快速恢复。
2. 序列化的使用场景示例
将用户集合保存到文件
假设你有一个 User 类:
public class User {
String name;
int age;
// ... 其他字段
}
并且你有一个用户列表:
List<User> users = new ArrayList<>();
users.add(new User("Basil", 25));
users.add(new User("Mary", 30));
// ... 等等
要把这个列表保存到文件中,可以将其序列化。需要时再反序列化,就能得到包含相同用户的同一个列表。请注意,类 User(以及它的所有字段)必须支持序列化,也就是要实现 Serializable。
在客户端和服务器之间传递消息
一个经典示例是聊天。用户输入消息,Message 对象被序列化并通过网络发送。服务器接收字节流、反序列化为对象、处理后可能再转发。
import java.io.*;
import java.net.*;
// Message 必须实现 Serializable
class Message implements Serializable {
String text;
Message(String text) {
this.text = text;
}
}
// 服务器
class Server {
public static void main(String[] args) throws Exception {
try (ServerSocket serverSocket = new ServerSocket(5000)) {
System.out.println("服务器正在等待连接...");
Socket socket = serverSocket.accept();
System.out.println("客户端已连接!");
try (ObjectInputStream in = new ObjectInputStream(socket.getInputStream())) {
Message msg = (Message) in.readObject();
System.out.println("收到消息: " + msg.text);
}
}
}
}
// 客户端
class Client {
public static void main(String[] args) throws Exception {
try (Socket socket = new Socket("localhost", 5000)) {
try (ObjectOutputStream out = new ObjectOutputStream(socket.getOutputStream())) {
Message msg = new Message("你好,服务器!");
out.writeObject(msg);
System.out.println("消息已发送!");
}
}
}
}
工作原理:
- 先启动 Server(它等待连接)。
- 然后启动 Client(它连接到 "localhost:5000")。
- 客户端序列化 Message 对象并通过套接字发送。
- 服务器接收字节流、反序列化并打印文本。
这里我们使用套接字(ServerSocket、Socket)——这是一种网络交互机制,你将在后续学习。当前更重要的不是网络细节,而是这个思路:客户端创建 Message 对象,序列化后发送;服务器接收字节流,反序列化回对象并打印消息。因此,即使你暂时不清楚 ServerSocket 和 Socket 是什么,这个例子也展示了序列化的价值:它让我们可以“打包”对象,通过网络传输,并在另一端无需额外转换就能还原。
对象缓存
在大型应用中常通过缓存来加速。例如,将复杂计算的结果序列化并存入缓存(文件、数据库、分布式存储)。下次请求时,只需反序列化对象即可快速恢复结果。
import java.io.*;
// 我们想要缓存的计算结果
class Result implements Serializable {
int value;
Result(int value) {
this.value = value;
}
}
public class CacheExample {
private static final String CACHE_FILE = "cache.dat";
public static void main(String[] args) throws Exception {
Result result;
// 检查是否存在缓存
File file = new File(CACHE_FILE);
if (file.exists()) {
// 从缓存中加载结果
try (ObjectInputStream in = new ObjectInputStream(new FileInputStream(file))) {
result = (Result) in.readObject();
System.out.println("已从缓存加载: " + result.value);
}
} else {
// “耗时”的计算(示例中只是求一个数的平方)
int x = 12345;
System.out.println("正在计算...(这会很久)");
result = new Result(x * x);
// 将结果保存到缓存
try (ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream(file))) {
out.writeObject(result);
System.out.println("已保存到缓存: " + result.value);
}
}
}
}
3. 序列化的限制与风险
序列化是一种强大的工具,但也有其陷阱。下面来讨论主要的限制与风险。
并非所有对象都可以序列化
在 Java 中,并不是所有对象都能“开箱即用”地被序列化。比如,与外部资源相关的对象(文件、网络连接、I/O 流)不能序列化。这很合理:你无法序列化一个“打开的文件”或“活跃的网络连接”——它们的状态依赖于操作系统和运行环境。
示例:带有 FileInputStream 类型字段的类无法序列化——尝试序列化会抛出错误。
安全性问题
序列化可能成为安全薄弱点。如果你反序列化来自不可靠来源(例如互联网)的数据,攻击者可能会投递恶意的字节流,导致程序出现意外行为,甚至执行恶意代码。
规则:切勿反序列化来自不受信任来源的数据!这就像收下一个来路不明的包裹——里面可能是什么都有。
版本兼容性
如果你修改了类的结构(例如添加或删除字段),先前序列化的对象可能与新版本的类不兼容,从而在反序列化时产生错误。更多细节将在后续课程中介绍。
性能
Java 的二进制序列化速度相当快,但有时不是最紧凑,也不总是方便与其他编程语言互通。与外部系统交互时,通常会使用文本格式(JSON、XML)。
4. 初学序列化时的常见错误
错误 1:试图序列化未实现接口 Serializable 的对象。
结果会抛出 NotSerializableException 异常。别忘了在类上显式声明 implements Serializable,并确保所有字段也可序列化!
错误 2:序列化包含不可序列化字段的对象。
如果类包含不支持序列化的字段类型(例如流或数据库连接),序列化将失败。解决方案是将这些字段标记为 transient(稍后详述)。
错误 3:从不可靠来源反序列化数据。
这可能导致安全漏洞,甚至执行恶意代码。只信任由你自己的程序序列化的数据!
错误 4:对象序列化后修改了类的结构。
如果你先保存了对象,随后在类中添加或删除字段,那么在反序列化时会出现错误或产生“奇怪”的值。更多内容将在后续课程中展开。
GO TO FULL VERSION