自定义序列化
默认的序列化行为不能满足所有需求。
比如:密码想序列化,但不想明文存储。或者字段是自定义类型,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 的类中,声明 private 的 writeObject 和 readObject 方法,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 / readObject | Externalizable |
|---|---|---|
| 默认字段序列化 | ✅ 自动 | ❌ 完全手动 |
| 钩子方法 | 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 - 先用默认方案,确实不够再自定义
