序列化
问题
什么是 Java 序列化?Serializable 和 Externalizable 的区别?transient 关键字有什么作用?
答案
序列化基础
序列化是将对象转换为字节流的过程,反序列化是将字节流还原为对象。主要用于网络传输、持久化存储、深拷贝等场景。
SerializationDemo.java
// 实现 Serializable 接口(标记接口,无抽象方法)
public class User implements Serializable {
private static final long serialVersionUID = 1L; // 版本号
private String name;
private int age;
private transient String password; // transient 不参与序列化
}
// 序列化:对象 → 字节流
User user = new User("Alice", 25, "secret");
try (ObjectOutputStream oos = new ObjectOutputStream(
new FileOutputStream("user.dat"))) {
oos.writeObject(user);
}
// 反序列化:字节流 → 对象
try (ObjectInputStream ois = new ObjectInputStream(
new FileInputStream("user.dat"))) {
User restored = (User) ois.readObject();
System.out.println(restored.getName()); // "Alice"
System.out.println(restored.getPassword()); // null(transient 字段)
}
serialVersionUID
serialVersionUID 用于验证序列化和反序列化的类版本一致性:
- 如果不显式声明,JVM 会根据类的结构自动生成(字段、方法等变化会导致 UID 变化)
- 反序列化时,如果当前类的
serialVersionUID与字节流中的不一致,会抛出InvalidClassException
强烈建议显式声明 serialVersionUID
如果不声明,类的任何修改(哪怕只是增加一个方法)都会导致自动生成的 UID 变化,之前序列化的数据将无法反序列化。
transient 关键字
transient 修饰的字段不参与序列化:
public class Session implements Serializable {
private String sessionId;
private transient long loginTime; // 不序列化
private transient Socket connection; // 不序列化(Socket 本身也不可序列化)
}
适用场景:敏感信息(密码)、临时数据、不可序列化的引用。
自定义序列化
通过实现 writeObject 和 readObject 方法自定义序列化逻辑:
CustomSerialization.java
public class Account implements Serializable {
private String username;
private transient String password; // 标记 transient 但通过自定义逻辑加密序列化
// 自定义序列化:加密密码后写入
private void writeObject(ObjectOutputStream oos) throws IOException {
oos.defaultWriteObject(); // 先序列化非 transient 字段
oos.writeObject(encrypt(password)); // 手动写入加密后的密码
}
// 自定义反序列化:读取并解密
private void readObject(ObjectInputStream ois)
throws IOException, ClassNotFoundException {
ois.defaultReadObject();
this.password = decrypt((String) ois.readObject());
}
}
主流序列化方案对比
| 方案 | 特点 | 大小 | 速度 | 跨语言 |
|---|---|---|---|---|
| Java 原生 | 内置,无需依赖 | 大 | 慢 | ❌ |
| JSON(Jackson/Gson) | 可读性好 | 较大 | 中等 | ✅ |
| Protobuf | 二进制,高效 | 最小 | 最快 | ✅ |
| Hessian | 二进制,Dubbo 默认 | 小 | 快 | 有限 |
| Kryo | 二进制,Java 生态 | 小 | 快 | ❌ |
实际项目中
- REST API:JSON(Jackson)
- RPC 框架:Protobuf(gRPC)、Hessian(Dubbo)
- 缓存/MQ:JSON 或 Protobuf
- Java 原生序列化:几乎不用于生产(性能差、安全风险)
常见面试问题
Q1: 为什么不推荐使用 Java 原生序列化?
答案:
- 安全风险:反序列化可以执行任意代码(反序列化漏洞),是 OWASP Top 10 之一
- 性能差:序列化/反序列化速度和产物大小都不如 JSON、Protobuf
- 不跨语言:只有 Java 能解析
- 版本兼容性差:类结构变化容易导致反序列化失败
Q2: Serializable 和 Externalizable 的区别?
答案:
| 维度 | Serializable | Externalizable |
|---|---|---|
| 序列化方式 | 自动序列化所有非 transient 字段 | 必须手动实现 writeExternal/readExternal |
| 性能 | 较低(反射) | 较高(手动控制) |
| 灵活性 | 简单,通过 transient 排除字段 | 完全自定义 |
| 构造器要求 | 无要求 | 必须有无参构造器 |
Q3: static 字段会被序列化吗?
答案:
不会。static 字段属于类而非对象实例,序列化只处理实例字段。反序列化后 static 字段的值取决于当前 JVM 中类的状态,而不是序列化时的值。
Q4: 如果父类没有实现 Serializable,子类能序列化吗?
答案:
可以序列化子类自己声明的字段,但父类的字段不会被序列化。反序列化时,父类的字段会通过父类的无参构造器初始化为默认值。如果父类没有无参构造器,反序列化会失败。
Q5: 序列化如何实现深拷贝?
答案:
通过序列化再反序列化,可以得到一个完全独立的对象副本:
public static <T extends Serializable> T deepCopy(T obj) throws Exception {
ByteArrayOutputStream baos = new ByteArrayOutputStream();
try (ObjectOutputStream oos = new ObjectOutputStream(baos)) {
oos.writeObject(obj);
}
ByteArrayInputStream bais = new ByteArrayInputStream(baos.toByteArray());
try (ObjectInputStream ois = new ObjectInputStream(bais)) {
return (T) ois.readObject();
}
}
简单但性能差,生产中通常用 JSON 序列化/反序列化实现,或手动 clone。