简介:本文系统讲解Java序列化与反序列化的核心概念与底层规则,深入剖析Serializable与Externalizable两套机制的差异、transient关键字的作用边界、构造函数的调用规则以及自定义序列化技巧,并结合典型代码示例给出选型建议与常见陷阱规避方法,帮助开发者在对象持久化、网络传输等场景中写出更健壮的代码。
在Java中,序列化(Serialization)是指将内存中的对象转换为字节序列的过程;反序列化(Deserialization)则是与之相反的过程,即把字节序列重新恢复为Java对象。
一个对象只有变成字节流,才能脱离JVM内存的束缚,被写入磁盘、存入缓存、或者在网络中从一台机器传送到另一台机器。因此,序列化机制主要服务于以下几类场景:
一个类只要实现了 Serializable 接口,它的实例就具备了被序列化的能力。这是一个标记接口,本身不含任何方法,实现成本极低:
public class User implements Serializable {private static final long serialVersionUID = 1L;private String name;private int age;private transient String password; // 该字段不参与序列化// getter/setter 省略}
一旦类实现了该接口,它的所有属性都会被纳入序列化范围——包括 private 私有字段,也包括属性所引用的其他对象(前提是这些被引用的对象的类同样实现了 Serializable,否则会抛出 NotSerializableException)。如果不希望某个字段被序列化,用 transient 修饰即可。
具体的读写操作通过 ObjectInputStream 与 ObjectOutputStream 完成:
// 序列化:对象 -> 字节流try (ObjectOutputStream oos =new ObjectOutputStream(new FileOutputStream("user.bin"))) {oos.writeObject(user);}// 反序列化:字节流 -> 对象try (ObjectInputStream ois =new ObjectInputStream(new FileInputStream("user.bin"))) {User u = (User) ois.readObject();}
这是许多开发者容易忽略的细节。由于反序列化时对象完全依据字节流中存储的二进制数据来重建,并不走正常的对象创建流程,因此:
建议在每个可序列化类中显式声明 serialVersionUID。它是类的“版本号”,反序列化时会校验字节流中的版本号与当前类是否一致,不一致则抛出 InvalidClassException。若不显式声明,编译器会根据类结构自动生成,类的任何微小改动(如新增字段)都可能导致版本号变化,使旧数据无法反序列化。
使用 transient 阻止字段序列化虽然简单方便,但它是一种“全有或全无”的粗暴手段:被修饰的字段被完全隔离在序列化机制之外,反序列化后该字段只会是类型默认值(如 null、0),原始值彻底丢失。
如果需要更精细的控制,可以在类中提供 writeObject() 与 readObject() 这两个私有方法,JVM 会通过反射自动回调它们:
private void writeObject(ObjectOutputStream out) throws IOException {out.defaultWriteObject(); // 先按默认规则序列化其他字段out.writeObject(encrypt(password)); // 敏感字段加密后再写出}private void readObject(ObjectInputStream in)throws IOException, ClassNotFoundException {in.defaultReadObject();password = decrypt((String) in.readObject()); // 读入后解密还原}
借助这两个方法,开发者可以决定各字段如何序列化:可以完全不写出某些字段,也可以对敏感字段做加密处理,实现“既保护数据又不丢数据”的效果。
Externalizable 是 Serializable 的子接口,但它不是标记接口,要求开发者手动实现 writeExternal() 和 readExternal() 两个方法,自行决定哪些属性以何种方式写出和读回:
public class User implements Externalizable {private String name;private String password;public User() { } // 必须保留 public 无参构造函数@Overridepublic void writeExternal(ObjectOutput out) throws IOException {out.writeObject(name);// 密码不写出,或只写出加密后的值}@Overridepublic void readExternal(ObjectInput in)throws IOException, ClassNotFoundException {name = (String) in.readObject();}}
它有两个与 Serializable 截然不同的关键规则:
| 对比维度 | Serializable | Externalizable |
|---|---|---|
| 实现成本 | 低,标记接口即可 | 高,需手写读写方法 |
| 控制粒度 | 默认全量,可用 transient 排除 | 完全由代码控制 |
| transient | 有效 | 无效 |
| 构造函数要求 | 本类无需;父类未实现接口时需默认构造 | 必须 public 无参构造 |
实际开发中的选型建议:
序列化与反序列化是Java对象走向“可传输、可存储”状态的基础能力。Serializable 胜在简单,配合 transient 与 writeObject()/readObject() 可以覆盖绝大多数日常需求;Externalizable 胜在完全可控,适合对格式和性能有严格要求的场景。理解两者的构造函数调用差异、transient 的作用范围以及版本号机制,是避开序列化陷阱、写出健壮代码的关键。在实际项目中,还应结合安全性、兼容性与可维护性综合权衡,选择最适合当前业务的序列化方案。