CodeGym /课程 /JAVA 25 SELF /控制序列化流程:writeObject、readObject

控制序列化流程:writeObject、readObject

JAVA 25 SELF
第 43 级 , 课程 0
可用

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

在你的 writeObjectreadObject 方法内部,几乎总是需要调用 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:方法中抛出异常。 如果在 writeObjectreadObject 中发生异常,序列化或反序列化会被中断,对象将不能被正确保存/加载。

错误 4:忘记序列化/反序列化父类。 如果你的类继承自另一个可序列化的类,一定要调用 defaultWriteObject/defaultReadObject,否则父类的字段不会被保存。

错误 5:序列化了敏感数据。 如果忘记排除密码或其他私密数据,它们会进入序列化文件。请使用 transient 或手动不序列化它们。

1
任务
JAVA 25 SELF, 第 43 级, 课程 0
已锁定
魔法猫实体:带日志的保存与唤醒
魔法猫实体:带日志的保存与唤醒
1
任务
JAVA 25 SELF, 第 43 级, 课程 0
已锁定
玩家档案:保存要点,忘却细节
玩家档案:保存要点,忘却细节
评论
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION