transient 关键字深入
你有没有想过这个问题:
transient和static都不参与序列化,它们有什么区别?
这一节深入探究 transient 的行为,以及它和 static、自定义序列化的关系。
transient 的基本行为
transient 修饰的字段序列化时被跳过:
java
public class User implements Serializable {
private String name;
private transient String password; // 不序列化
private transient int cache; // 不序列化
}
// 序列化后:name 有值,password = null,cache = 0反序列化后得到类型默认值:
| 字段类型 | transient 后的默认值 |
|---|---|
int / long / short / byte | 0 / 0L / 0 / 0 |
float / double | 0.0f / 0.0d |
boolean | false |
| 对象引用 | null |
char | \u0000 |
transient vs static:两个独立维度
static 字段同样不参与序列化,但原因完全不同:
| 修饰符 | 序列化行为 | 原因 |
|---|---|---|
static | ❌ 不序列化 | 属于类,不属于对象 |
transient | ❌ 不序列化 | 明确声明不需要序列化 |
两者叠加时,transient 是多余的:
java
public class User implements Serializable {
private String name;
private static String className; // 不序列化(属于类)
private transient static int count; // static 已经不序列化,transient 多余
}记住:static 字段在序列化时代表类的共享状态,不是对象的个体状态。如果需要序列化 static 字段,需要自定义序列化逻辑。
transient 的典型应用场景
1. 敏感数据
java
public class User implements Serializable {
private String name;
private transient String password; // 密码不能序列化
private transient String ssn; // 身份证号
private transient String apiKey; // API 密钥
}2. 运行时数据
java
public class Config implements Serializable {
private String configPath;
private transient Connection dbConnection; // 数据库连接,序列化后无效
private transient Map<String, Object> cache; // 内存缓存
private transient volatile boolean initialized; // 运行时状态
}3. 可以重新计算的数据
java
public class Invoice implements Serializable {
private BigDecimal amount;
private BigDecimal tax;
private transient BigDecimal total; // = amount + tax,可以重新计算
}跳过 + 加密:transient 与自定义序列化
transient 只能完全跳过字段。但有时需要:跳过 + 在序列化/反序列化时做特殊处理。
比如:密码不想明文存储,但也不想完全丢弃(反序列化后还能用)。
这就要结合自定义序列化:
java
public class User implements Serializable {
private String name;
private transient String password; // 用自定义方式序列化
private void writeObject(ObjectOutputStream out) throws IOException {
out.defaultWriteObject(); // 先序列化默认字段(name)
out.writeObject(encrypt(password)); // 再序列化加密后的密码
}
private void readObject(ObjectInputStream in)
throws IOException, ClassNotFoundException {
in.defaultReadObject();
String encrypted = (String) in.readObject();
this.password = decrypt(encrypted); // 解密
}
private String encrypt(String s) { /* AES 加密实现 */ }
private String decrypt(String s) { /* AES 解密实现 */ }
}这里 password 虽然是 transient,但通过 writeObject / readObject 手动序列化——实现了「跳过 + 加密处理」的效果。
数组与 transient
数组中的元素序列化规则和普通字段一样:
java
public class Matrix implements Serializable {
private String name;
private transient double[][] data; // 整个数组跳过
private transient int[] tempBuffer; // 临时缓冲区
}如果只需要序列化数组的一部分,可以结合自定义序列化:
java
private void writeObject(ObjectOutputStream out) throws IOException {
out.defaultWriteObject();
out.writeInt(data.length); // 先写长度
out.writeObject(data); // 再写数据
}容易被忽视的点:反序列化不运行构造函数
反序列化时,JVM 不调用构造函数,直接用二进制数据构建对象。
这意味着:如果类依赖构造函数做初始化(如设置默认值、建立连接),反序列化后这些工作不会自动做。
需要在 readObject 中补充:
java
private void readObject(ObjectInputStream in)
throws IOException, ClassNotFoundException {
in.defaultReadObject();
// 补充构造函数中应该做的工作
if (this.cache == null) {
this.cache = new ConcurrentHashMap<>();
}
}总结
┌─────────────────────────────────────────────────────────────────┐
│ transient 的使用场景: │
│ │
│ 1. 敏感数据:密码、密钥、身份证号 │
│ 2. 运行时数据:缓存、连接、临时状态 │
│ 3. 可重新计算的数据:派生字段 │
│ │
│ transient + writeObject/readObject = 跳过 + 自定义处理 │
│ │
│ 口诀:敏感不存用 transient,要存但要处理用组合拳 │
└─────────────────────────────────────────────────────────────────┘