CodeGym /课程 /JAVA 25 SELF /配置序列化行为:自定义方法

配置序列化行为:自定义方法

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

1. writeReplace 与 readResolve:理论

有时标准的序列化机制并不够用。设想这样一种情况:你有一个单例(在整个程序中只能有一个实例的类),你希望反序列化后它依然是那个唯一实例(而不是出现一个新的克隆)。或者,你希望序列化的不是对象本身,而是它的“轻量”版本(代理),以隐藏实现细节或节省空间。

在 Java 中为此提供了专用方法:writeReplacereadResolve。它们的职责是——将即将被序列化或刚被反序列化的对象替换为另一个对象。

一个简单的类比:
这就好比你要给朋友寄包裹,但你把自己的玩具替身放进了箱子。等朋友拆开包裹时,手里的“玩具”却变成了真正的你!(现实里当然做不到,但在 Java 里——可以。)

writeReplace

方法 private Object writeReplace() 会在对象序列化之前被调用。它可以返回任意对象,该对象将实际被序列化来替代原始对象。如果未实现——就序列化对象本身。

签名:


private Object writeReplace() throws ObjectStreamException

readResolve

方法 private Object readResolve() 会在对象反序列化之后被调用。它允许将刚创建出来的对象替换为另一个对象(例如返回单例或缓存的实例)。

签名:

private Object readResolve() throws ObjectStreamException

重要:
这两个方法都必须是 private 且返回 Object。这是 Java 序列化规范的要求。如果将它们设为 public——序列化机制会直接忽略它们。

2. 在实践中使用 writeReplace 与 readResolve

单例与 readResolve

单例就是在整个程序中只能有一个实例的类。如果序列化这样的对象再恢复,若没有 readResolve,会出现一个新实例,从而破坏“唯一性”的约束。而使用 readResolve,就可以返回那个唯一对象,保持单例的语义。

import java.io.*;

public class MySingleton implements Serializable {
    private static final MySingleton INSTANCE = new MySingleton();
    private MySingleton() {}

    public static MySingleton getInstance() {
        return INSTANCE;
    }

    // 确保反序列化后返回的正是 INSTANCE
    private Object readResolve() throws ObjectStreamException {
        return INSTANCE;
    }
}

说明:
没有 readResolve 时,反序列化后会出现一个新对象,与原始单例按 == 比较不相等;使用 readResolve 时——总是返回 INSTANCE

实际验证:

MySingleton s1 = MySingleton.getInstance();
ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("singleton.bin"));
out.writeObject(s1);
out.close();

ObjectInputStream in = new ObjectInputStream(new FileInputStream("singleton.bin"));
MySingleton s2 = (MySingleton) in.readObject();
in.close();

System.out.println(s1 == s2); // 如果有 readResolve 为 true;没有则为 false

writeReplace:序列化代理对象

有时对象过于“沉重”,或者包含敏感数据,或者根本不该以完整形态对外暴露。这种情况下,可以序列化一个“替身”——代理对象。

示例:
假设我们有一个类 User,其中包含私有密码。我们不希望密码被序列化。

import java.io.*;

public class User implements Serializable {
    private String username;
    private transient String password; // transient — 不会被序列化

    public User(String username, String password) {
        this.username = username;
        this.password = password;
    }

    // 序列化 User 的替身:仅序列化 UserProxy
    private Object writeReplace() throws ObjectStreamException {
        return new UserProxy(username);
    }

    // 仅用于序列化的代理类
    private static class UserProxy implements Serializable {
        private String username;
        public UserProxy(String username) {
            this.username = username;
        }

        private Object readResolve() throws ObjectStreamException {
            // 现实中无法恢复密码——返回一个密码为空的 User
            return new User(username, "");
        }
    }
}

说明:

  • 序列化时,User 会被替换为 UserProxy(不包含密码)。
  • 反序列化时,UserProxy 会再变回 User(但密码为空)。

3. 不可变对象的自定义序列化

不可变对象通常使用私有的 final 字段且没有 setter。标准的 Java 序列化可以绕过这些限制,但有时通过 writeReplace/readResolve 更明确地控制过程更好。

示例:Value Object

import java.io.*;

public final class Money implements Serializable {
    private final int amount;
    private final String currency;

    public Money(int amount, String currency) {
        this.amount = amount;
        this.currency = currency;
    }

    private Object writeReplace() throws ObjectStreamException {
        return new MoneyProxy(amount, currency);
    }

    private static class MoneyProxy implements Serializable {
        private final int amount;
        private final String currency;

        MoneyProxy(int amount, String currency) {
            this.amount = amount;
            this.currency = currency;
        }

        private Object readResolve() throws ObjectStreamException {
            return new Money(amount, currency);
        }
    }
}

说明:

  • 序列化时,Money 会被替换为 MoneyProxy(一个 POJO)。
  • 反序列化时,MoneyProxy 会再变回 Money

与 writeObject/readObject 的交互

writeReplace/readResolvewriteObject/readObject 相互独立。如果两套机制都定义了,会先调用 writeReplace,随后对返回的对象再调用 writeObject(前提是它实现了 Serializable)。

示意图:

flowchart LR
    A[对象] -- writeReplace --> B[代理对象]
    B -- writeObject --> C[字节流]
    C -- readObject --> D[代理对象]
    D -- readResolve --> E[最终对象]

4. 实践:带对象替换的序列化

让我们在你的练习应用中加入自定义序列化——例如对类 Person,让序列化时只写入姓名而忽略年龄(假设我们出于隐私考虑)。

步骤 1. 主类

import java.io.*;

public class Person implements Serializable {
    private String name;
    private int age; // 不希望被序列化

    public Person(String name, int age) {
        this.name = name;
        this.age = age;
    }

    private Object writeReplace() throws ObjectStreamException {
        return new PersonProxy(name);
    }

    private static class PersonProxy implements Serializable {
        private final String name;

        PersonProxy(String name) {
            this.name = name;
        }

        private Object readResolve() throws ObjectStreamException {
            return new Person(name, -1); // -1 — "年龄未知"
        }
    }

    @Override
    public String toString() {
        return "Person{name='" + name + "', age=" + age + "}";
    }
}

步骤 2. 测试

public class TestCustomSerialization {
    public static void main(String[] args) throws Exception {
        Person original = new Person("Alice", 30);

        // 序列化
        ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("person.bin"));
        out.writeObject(original);
        out.close();

        // 反序列化
        ObjectInputStream in = new ObjectInputStream(new FileInputStream("person.bin"));
        Person deserialized = (Person) in.readObject();
        in.close();

        System.out.println("序列化之前: " + original);
        System.out.println("反序列化之后: " + deserialized);
    }
}

结果:

序列化之前: Person{name='Alice', age=30}
反序列化之后: Person{name='Alice', age=-1}

如你所见,年龄没有被序列化——一切按计划进行!

5. 特点与细节

何时使用 writeReplace/readResolve?

  • 当只需序列化对象的部分状态时。
  • 用于代理对象的序列化/反序列化。
  • 用于支持模式 Singleton
  • 用于不可变或复杂对象,其内部结构可能发生变化。

何时不应使用?

  • 若可通过 transient 字段或 writeObject/readObject 解决。
  • 若对象不应被替换为其他对象。

与继承的兼容性

如果父类定义了 writeReplace/readResolve,则它们也会在子类上被调用(除非被重写)。在层次结构中要小心!

6. 自定义序列化的常见错误

错误 #1:方法可见性不正确。 如果将 writeReplace/readResolve 设为非 private,序列化机制不会调用它们。必须是 private

错误 #2:返回类型不匹配。 writeReplace/readResolve 必须返回 Object。即使实际上返回的是你的自定义类型——方法也要声明为返回 Object

错误 #3:数据丢失。 如果代理对象不包含恢复原始对象所需的全部数据,信息会丢失。务必确保能够将对象恢复回来。

错误 #4:破坏不变量。 readResolve 应该返回符合程序期望的对象(例如对于单例——必须返回 INSTANCE)。

错误 #5:未处理的异常。 writeReplace/readResolve 可能抛出 ObjectStreamException。请处理或显式抛出它。

评论 (1)
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION
ncksllpo 级别 44,Cherkasy,Ukraine
14 七月 2026
老是忘记throws 😅