跳到主要内容

序列化

问题

什么是 Java 序列化?SerializableExternalizable 的区别?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 本身也不可序列化)
}

适用场景:敏感信息(密码)、临时数据、不可序列化的引用。

自定义序列化

通过实现 writeObjectreadObject 方法自定义序列化逻辑:

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 原生序列化?

答案

  1. 安全风险:反序列化可以执行任意代码(反序列化漏洞),是 OWASP Top 10 之一
  2. 性能差:序列化/反序列化速度和产物大小都不如 JSON、Protobuf
  3. 不跨语言:只有 Java 能解析
  4. 版本兼容性差:类结构变化容易导致反序列化失败

Q2: SerializableExternalizable 的区别?

答案

维度SerializableExternalizable
序列化方式自动序列化所有非 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。

相关链接