Skip to content

自定义序列化

默认的序列化行为不能满足所有需求。

比如:密码想序列化,但不想明文存储。或者字段是自定义类型,JVM 不知道怎么序列化。

这一节讲解三种自定义序列化方案。

需求提出

假设有这样一个场景:

java
public class User implements Serializable {
    private String name;
    private String password; // 存着,但序列化时不想明文
    private List<String> roles; // 角色列表,也想加密
}

默认序列化会把 password 和 roles 直接写入流——谁拿到文件都能看到明文密码。

默认方案的局限

┌─────────────────────────────────────────────────────────────────┐
│  默认序列化的局限:                                               │
│                                                                 │
│  1. 字段按原样序列化,无法做加密/压缩                             │
│  2. 某些类型(如 Connection、Thread)无法序列化                   │
│  3. 无法控制序列化的格式和顺序                                   │
│  4. 无法实现复杂的版本兼容逻辑                                   │
└─────────────────────────────────────────────────────────────────┘

方案一:writeObject / readObject

在实现了 Serializable 的类中,声明 privatewriteObjectreadObject 方法,JVM 就会在序列化和反序列化时调用它们。

java
public class User implements Serializable {
    private static final long serialVersionUID = 1L;
    private String name;
    private String password;           // 存储时加密
    private List<String> roles;        // 序列化时加密

    private void writeObject(ObjectOutputStream out) throws IOException {
        out.defaultWriteObject(); // 先写默认字段(name)
        out.writeObject(encrypt(password)); // 再写加密后的密码
        out.writeObject(roles.stream().map(this::encrypt).collect(Collectors.toList()));
    }

    private void readObject(ObjectInputStream in)
            throws IOException, ClassNotFoundException {
        in.defaultReadObject(); // 先读默认字段
        this.password = decrypt((String) in.readObject());
        this.roles = ((List<String>) in.readObject()).stream()
                .map(this::decrypt).collect(Collectors.toList());
    }

    private String encrypt(String s) { /* AES 加密 */ }
    private String decrypt(String s) { /* AES 解密 */ }
}

要点

  • defaultWriteObject() 调用默认的序列化行为(序列化所有非 transient 字段)
  • 必须在 defaultWriteObject() 之后写入额外数据
  • 必须在 defaultReadObject() 之后读取额外数据
  • 读写顺序必须严格一致

方案二:Externalizable

Externalizable 继承自 Serializable,但序列化逻辑完全由你决定——JVM 不会自动序列化任何字段。

必须有空构造方法

java
public class User implements Externalizable {
    private String name;
    private int age;

    public User() { } // 必须有空构造!

    @Override
    public void writeExternal(ObjectOutput out) throws IOException {
        out.writeObject(name);
        out.writeInt(age);
    }

    @Override
    public void readExternal(ObjectInput in)
            throws IOException, ClassNotFoundException {
        this.name = (String) in.readObject();
        this.age = in.readInt();
    }
}

Externalizable 的版本兼容

Externalizable 不会自动处理 serialVersionUID,所以如果需要版本兼容,需要在写入时记录版本号:

java
public class Message implements Externalizable {
    private static final int VERSION = 1;
    private String content;
    private String timestamp; // V2 新增字段

    public Message() { }

    @Override
    public void writeExternal(ObjectOutput out) throws IOException {
        out.writeInt(VERSION);
        out.writeObject(content);
        if (VERSION >= 1) {
            out.writeObject(timestamp); // V2 才写入
        }
    }

    @Override
    public void readExternal(ObjectInput in)
            throws IOException, ClassNotFoundException {
        int version = in.readInt();
        this.content = (String) in.readObject();
        if (version >= 1) {
            this.timestamp = (String) in.readObject(); // V2 才读
        }
    }
}

方案对比

特性writeObject / readObjectExternalizable
默认字段序列化✅ 自动❌ 完全手动
钩子方法private,JVM 自动调用public,需自己调用
transient 字段自动跳过完全手动
版本兼容性自动处理 UID需手动管理
控制粒度在默认行为上做增强完全掌控字节流格式
适用场景加密、压缩、增强默认行为协议序列化、精确格式控制

场景选型

┌─────────────────────────────────────────────────────────────────┐
│  90% 的场景:Serializable + writeObject / readObject            │
│           ——在默认行为上做增强,满足大多数需求                   │
│                                                                 │
│  10% 的场景:Externalizable                                      │
│           ——需要精确控制二进制格式,如自定义通信协议             │
└─────────────────────────────────────────────────────────────────┘

实际案例:序列化版本号字段

有时想在序列化时记录当时使用的 serialVersionUID:

java
public class Message implements Serializable {
    private static final long serialVersionUID = 1L;
    private String content;
    private int serializationVersion; // 记录序列化时的版本

    private void writeObject(ObjectOutputStream out) throws IOException {
        out.defaultWriteObject();
        out.writeInt(serialVersionUID.intValue());
    }

    private void readObject(ObjectInputStream in)
            throws IOException, ClassNotFoundException {
        in.defaultReadObject();
        this.serializationVersion = in.readInt();
    }
}

总结

自定义序列化是「进阶技能」,不是每个类都需要。当默认行为无法满足需求时:

  • 加密/压缩:选 writeObject / readObject
  • 精确格式控制:选 Externalizable
  • 先用默认方案,确实不够再自定义

基于 VitePress 构建