1. 介绍
自动序列化就像飞机上的自动驾驶:一切按计划进行时运转良好。但一旦出现特殊情况,就会发现简单的机制已经不够用了。设想你需要保存一个对象,但并不想存储它的所有字段:有些数据是临时的,有些则过于敏感不应写入文件。或者相反——在保存时需要加点“自己的东西”:例如版本号或校验和。有时在写入或读取数据前还需要执行校验或转换。还有更复杂的情况:当类的结构随时间改变时,如何确保与旧版本的兼容性。
在这些情况下就会明显感觉到:仅靠标准序列化是不够的,需要自己接管。
序列化的特殊方法:writeObject 和 readObject
在 Java 中有两个特殊的方法,可以完全控制对象的序列化与反序列化过程:
private void writeObject(ObjectOutputStream out) throws IOException
private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException
重要!
- 方法必须是 private(不是 public、不是 protected,也不是 package-private)。
- 方法签名必须与上面完全一致。
- 如果在你的类中声明了这些方法,它们会替代标准的序列化/反序列化被调用。
它是如何工作的?
当你调用 ObjectOutputStream.writeObject(object) 时,JVM 会先在 object 的类中查找方法 private void writeObject(ObjectOutputStream)。如果存在,就调用它。类似地,反序列化时会调用 private void readObject(ObjectInputStream)。
如果未声明这些方法,则使用默认序列化。
writeObject 和 readObject 的结构
方法签名
private void writeObject(ObjectOutputStream out) throws IOException
private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException
在这些方法内部,你必须调用:
- out.defaultWriteObject(); — 用于序列化父类和当前类的标准字段(非 transient)。
- in.defaultReadObject(); — 用于反序列化标准字段。
如果不调用这些方法,标准字段将不会被序列化——反序列化后对象会变得“空空如也”。这就像忘了把护照放进行李箱:名义上你到了,但无法证明你是谁。
2. 示例:在序列化时添加校验和
来看一个实际示例。假设我们有一个用户类,希望在序列化时给对象添加校验和,以便在反序列化时校验数据完整性。
import java.io.*;
public class User implements Serializable {
private static final long serialVersionUID = 1L;
private String name;
private int age;
// transient 字段 — 不进行序列化
private transient int checksum;
public User(String name, int age) {
this.name = name;
this.age = age;
this.checksum = calculateChecksum();
}
private int calculateChecksum() {
return (name != null ? name.hashCode() : 0) + age;
}
// 自定义序列化
private void writeObject(ObjectOutputStream out) throws IOException {
out.defaultWriteObject(); // 保存标准字段
int sum = calculateChecksum();
out.writeInt(sum); // 写入校验和
System.out.println("[LOG] 序列化 User: checksum=" + sum);
}
// 自定义反序列化
private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException {
in.defaultReadObject(); // 恢复标准字段
int sum = in.readInt(); // 读取校验和
int actual = calculateChecksum();
System.out.println("[LOG] 反序列化 User: checksum=" + sum + ", actual=" + actual);
if (sum != actual) {
throw new IOException("Data corrupted! Checksum mismatch.");
}
this.checksum = actual;
}
@Override
public String toString() {
return "User{name='" + name + "', age=" + age + ", checksum=" + checksum + "}";
}
}
使用示例:
// 保存对象
User user = new User("Alice", 42);
try (ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("user.bin"))) {
out.writeObject(user);
}
// 读取对象
try (ObjectInputStream in = new ObjectInputStream(new FileInputStream("user.bin"))) {
User loaded = (User) in.readObject();
System.out.println("恢复的对象: " + loaded);
}
发生了什么?
- 序列化时调用 writeObject,保存标准字段和校验和。
- 反序列化时调用 readObject,恢复字段并校验校验和。
- 控制台会打印日志;如果有问题——将抛出异常。
3. 从序列化中排除敏感数据
有时需要某些字段不被序列化(例如密码)。为此可以使用关键字 transient(更多内容见下一讲),也可以在实现 writeObject 时手动不序列化该字段。
示例:
public class Account implements Serializable {
private static final long serialVersionUID = 1L;
private String username;
private transient String password; // transient — 不会被序列化
// 也可以这样做:
private void writeObject(ObjectOutputStream out) throws IOException {
out.defaultWriteObject();
// 不写入 password!
}
private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException {
in.defaultReadObject();
// password 保持为 null
}
}
注意:
如果你只想序列化对象的一部分——不要把多余的字段写入流即可。
4. 调用父类相关方法:defaultWriteObject 和 defaultReadObject
在你的 writeObject 和 readObject 方法内部,几乎总是需要调用 defaultWriteObject() 和 defaultReadObject()。这就像在添加自己的备注前先“保存草稿”。
这些方法负责对当前类及其父类的所有非-transient、非-static 字段进行默认序列化。如果不调用,这些字段将不会被序列化,反序列化后会是空的。
错误示例:
private void writeObject(ObjectOutputStream out) throws IOException {
// out.defaultWriteObject(); // 忘记调用了!
out.writeInt(123); // 一些自定义内容
}
在这种情况下,标准字段根本不会被保存!
5. 实践:为序列化过程添加日志
我们给用户类添加日志,以便看到何时进行序列化与反序列化。
public class Person implements Serializable {
private static final long serialVersionUID = 1L;
private String name;
private int age;
private void writeObject(ObjectOutputStream out) throws IOException {
System.out.println("[LOG] 序列化 Person: " + name + ", age " + age);
out.defaultWriteObject();
}
private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException {
in.defaultReadObject();
System.out.println("[LOG] 反序列化 Person: " + name + ", age " + age);
}
}
用法:
Person p = new Person("Bob", 30);
// 保存到文件
try (ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("person.bin"))) {
out.writeObject(p);
}
// 从文件加载
try (ObjectInputStream in = new ObjectInputStream(new FileInputStream("person.bin"))) {
Person loaded = (Person) in.readObject();
}
结果:
你会在控制台看到对象被序列化与反序列化的日志。
6. 使用 writeObject/readObject 时的常见错误
错误 1:未调用 defaultWriteObject/defaultReadObject。 如果忘记调用这些方法,标准字段不会被序列化,反序列化后对象会是空的或不正确的。
错误 2:方法签名不正确。 方法必须严格是 private void writeObject(ObjectOutputStream) 和 private void readObject(ObjectInputStream)。如果将其改为 public/protected 或修改了参数——就不会被自动调用。
错误 3:方法中抛出异常。 如果在 writeObject 或 readObject 中发生异常,序列化或反序列化会被中断,对象将不能被正确保存/加载。
错误 4:忘记序列化/反序列化父类。 如果你的类继承自另一个可序列化的类,一定要调用 defaultWriteObject/defaultReadObject,否则父类的字段不会被保存。
错误 5:序列化了敏感数据。 如果忘记排除密码或其他私密数据,它们会进入序列化文件。请使用 transient 或手动不序列化它们。
GO TO FULL VERSION