在Java中,对象的序列化是指将堆内存里的对象状态转换为字节序列,以便存储到文件、数据库或通过网络传输;反序列化则是把字节序列恢复成原来的对象。JDK自带的ObjectOutputStream和ObjectInputStream就是专门处理这种操作的原生对象流类,位于java.io包下,是很多缓存框架和远程调用组件的基础类库。

一、Serializable接口与基础准备
要让一个类的对象能被ObjectOutputStream写出,这个类必须实现java.io.Serializable接口。Serializable是一个标记接口,内部没有任何方法,仅用于告诉JVM该类的对象允许被序列化。如果尝试序列化未实现该接口的对象,运行时会抛出NotSerializableException。
除了实现接口,建议显式声明一个serialVersionUID静态常量。它是类的版本标识,JVM在反序列化时会比对字节流中的版本号和当前类的版本号,不一致则抛InvalidClassException。不显式声明时,编译器会根据类结构自动生成,类稍有改动就可能导致版本号变化,从而引发反序列化失败。
import java.io.Serializable;
// 实现Serializable标记接口,并显式定义版本号
public class User implements Serializable {
private static final long serialVersionUID = 1L;
private String name;
private int age;
// 使用transient修饰的字段不会被序列化
private transient String tempToken;
public User(String name, int age, String tempToken) {
this.name = name;
this.age = age;
this.tempToken = tempToken;
}
// 省略getter和setter
}
二、使用ObjectOutputStream写出对象
ObjectOutputStream需要包裹一个底层的字节输出流,例如FileOutputStream用于写文件,或Socket.getOutputStream()用于网络发送。构造后调用writeObject方法即可把对象序列化为字节并写出。写完应调用flush确保数据全部落盘,并在finally块中关闭流。
下面示例将User对象保存到本地文件。注意写多个对象时要按同一顺序写入,后续读取也必须按相同顺序读取,否则会出现流错位或类型转换异常。另外,被transient修饰的tempToken字段在序列化后值会丢失,反序列化得到null。
import java.io.FileOutputStream;
import java.io.ObjectOutputStream;
import java.io.IOException;
public class SerializeDemo {
public static void main(String[] args) {
User user = new User("张三", 28, "abc123");
try (FileOutputStream fos = new FileOutputStream("user.dat");
ObjectOutputStream oos = new ObjectOutputStream(fos)) {
oos.writeObject(user);
oos.flush();
System.out.println("对象已序列化到文件");
} catch (IOException e) {
e.printStackTrace();
}
}
}
三、使用ObjectInputStream读取对象
ObjectInputStream与ObjectOutputStream对应,它包裹底层的字节输入流(如FileInputStream),通过readObject方法把字节流还原成Object类型,使用时通常强转为具体类。读取顺序必须和写入顺序严格一致,且类路径下要有与原序列化版本号匹配的类定义。
若字节流中的serialVersionUID与当前类不同,或者类结构发生不兼容修改(如删除了非transient字段),就会抛出InvalidClassException。因此跨版本传输对象时,维护好serialVersionUID尤为重要。以下代码演示从文件读回User对象:
import java.io.FileInputStream;
import java.io.ObjectInputStream;
import java.io.IOException;
import java.io.ClassNotFoundException;
public class DeserializeDemo {
public static void main(String[] args) {
try (FileInputStream fis = new FileInputStream("user.dat");
ObjectInputStream ois = new ObjectInputStream(fis)) {
User user = (User) ois.readObject();
System.out.println("姓名:" + user.getName());
System.out.println("年龄:" + user.getAge());
System.out.println("临时令牌:" + user.getTempToken());
} catch (IOException | ClassNotFoundException e) {
e.printStackTrace();
}
}
}
四、常用类库与注意事项对比
除了JDK原生对象流,实际工程中常配合commons-io等工具库简化流关闭,或采用Kryo、Protobuf等第三方序列化框架提升性能。但理解ObjectInputStream与ObjectOutputStream的机制仍是基础。下表列出原生对象流的一些关键特性与限制:
| 特性 | 说明 |
|---|---|
| 依赖接口 | 被序列化类必须实现Serializable,否则抛NotSerializableException |
| transient字段 | 标记后不参与序列化,反序列化得到默认值或null |
| serialVersionUID | 建议显式声明,控制类版本兼容 |
| 性能 | 原生序列化速度较慢且体积大,不适合高频网络传输 |
在简单本地缓存、教学示例或内部系统间短距离对象传递时,ObjectOutputStream和ObjectInputStream足够易用。但在高并发服务中,建议评估更高效的二进制协议,以避免原生序列化带来的性能瓶颈。
五、典型错误与排查思路
开发者常遇到的第一个问题是忘记实现Serializable接口,此时写对象立刻报错。第二个问题是读写顺序不对应,例如先写两个对象却只读一个,后续再读其他类型就会报StreamCorruptedException。第三个问题是改了类没改serialVersionUID,导致旧数据无法反序列化。
排查时可以先确认类路径中的类是否实现接口,再检查序列化与反序列化代码中的写出和读取顺序是否完全对称。若涉及版本升级,应保留旧serialVersionUID并做字段兼容,比如用transient或自定义readObject方法处理新增字段的默认值。这样可以在不破坏旧数据的前提下完成类演进。
Java序列化ObjectInputStreamObjectOutputStream修改时间:2026-08-01 21:45:29