ObjectInputStream / ObjectOutputStream
你有没有想过这个问题:为什么 writeObject() 能把一个 Java 对象转成字节流,又能把字节流转回对象?
答案藏在流格式里。
流格式:不止是对象数据
当你调用 writeObject() 写入一个对象时,写入的内容远不止对象字段的值:
┌────────────────────────────────────────────────────────────────┐
│ ObjectOutputStream 写入的字节序列包含: │
│ │
│ 1. 魔数(stream magic):0xAC ED ——「这是一个序列化流」 │
│ 2. 版本号(stream version):0x00 05 —— 流格式版本 │
│ 3. 类描述符:类名 + serialVersionUID │
│ 4. 字段值:每个字段的序列化值 │
│ 5. 结束标记:TC_ENDBLOCKDATA │
└────────────────────────────────────────────────────────────────┘这解释了为什么 Java 原生序列化比 JSON 大——它携带了自描述信息,保证版本兼容性。
基本用法
java
// 序列化:写入对象
User user = new User("张三", 25, "pass123");
try (ObjectOutputStream oos = new ObjectOutputStream(
new BufferedOutputStream(
new FileOutputStream("user.dat")))) {
oos.writeObject(user);
}
// 反序列化:读取对象
try (ObjectInputStream ois = new ObjectInputStream(
new BufferedInputStream(
new FileInputStream("user.dat")))) {
User restored = (User) ois.readObject();
System.out.println(restored.getName()); // 张三
}写入多个对象
写入多个对象到同一个流,反序列化时按写入顺序读取:
java
try (ObjectOutputStream oos = new ObjectOutputStream(
new FileOutputStream("data.obj"))) {
oos.writeObject(new User("张三", 20));
oos.writeObject(new User("李四", 25));
oos.writeObject(new User("王五", 30));
}
try (ObjectInputStream ois = new ObjectInputStream(
new FileInputStream("data.obj"))) {
while (true) {
try {
User u = (User) ois.readObject();
System.out.println(u.getName());
} catch (EOFException e) {
break; // 读到流末尾,正常结束
}
}
}注意:通过捕获 EOFException 来判断流是否结束——这是标准做法。不是 readObject() 返回 null,它从来不返回 null,只抛 EOFException。
readObject 可能抛出的异常
| 异常 | 触发场景 |
|---|---|
ClassNotFoundException | 读取的类在 classpath 中不存在 |
InvalidClassException | serialVersionUID 不匹配 |
OptionalDataException | 读取的位置不是对象(如读取了 primitive 数据) |
EOFException | 流已结束,无更多数据 |
java
try (ObjectInputStream ois = new ObjectInputStream(
new FileInputStream("data.obj"))) {
while (true) {
try {
Object obj = ois.readObject();
// 处理对象
} catch (ClassNotFoundException e) {
System.err.println("类不存在: " + e.getMessage());
} catch (EOFException e) {
break; // 正常结束
}
}
}引用链:对象图的序列化
如果对象 A 持有对象 B 的引用,对象 B 又持有对象 C 的引用,序列化时 JVM 怎么处理的?
答案是对象表机制(Object Table):
java
public class Department implements Serializable {
private String name;
private List<Employee> employees;
}
public class Employee implements Serializable {
private String name;
private Department department; // 反向引用,形成循环
}序列化时,JVM 维护一个对象表,每个对象只序列化一次。如果后续遇到相同引用,就写入表中的索引号,而不是重新序列化整个对象。
这保证了引用关系的完整性——序列化后再反序列化,引用链依然正确。
版本兼容机制
如果类增加了新字段,serialVersionUID 不变的情况下,JVM 认为可以兼容:
java
// V1:发布版本
public class User implements Serializable {
private static final long serialVersionUID = 1L;
private String name;
}
// V2:新增 age 字段
public class User implements Serializable {
private static final long serialVersionUID = 1L; // 没变!
private String name;
private int age; // 新字段
}读取旧 V1 数据时,age 字段没有值,JVM 设为 0(基本类型的默认值)。
但如果 serialVersionUID 不一致,直接抛 InvalidClassException——不会尝试兼容。
总结
┌─────────────────────────────────────────────────────────────────┐
│ ObjectOutputStream / ObjectInputStream 是 Java 原生序列化的核心 │
│ │
│ 记住三点: │
│ 1. 写入内容包含魔数、版本号、类描述——比纯数据大,但自描述 │
│ 2. 读取多个对象用 while + EOFException 判断结束 │
│ 3. 引用链自动维护,序列化一个对象等于序列化整个对象图 │
└─────────────────────────────────────────────────────────────────┘