Java序列化案例:从基础到实践的完整指南
目录导读
- 什么是Java序列化?为什么需要它?
- 序列化的核心机制与实现步骤
- 简单对象序列化与反序列化
- 自定义序列化与transient关键字
- 继承与序列化时的注意事项
- 常见问题与最佳实践
- 问答环节:解决你最常见的疑惑
什么是Java序列化?为什么需要它?
Java序列化是指将Java对象转换为字节序列(byte stream)的过程,以便存储到文件、数据库或通过网络传输,反序列化则是将字节序列恢复为原始对象的过程。

为什么需要序列化?
- 持久化存储:将对象状态保存到磁盘,以便下次启动时恢复。
- 远程通信:在分布式系统(如RMI、Socket、EJB)中,通过网络传递对象。
- 深拷贝:通过序列化实现对象的完整复制,避免引用共享。
- 缓存:将对象写入Redis、Memcached等缓存系统。
关键点:只有实现了java.io.Serializable接口的类才能被序列化(该接口是一个标记接口,不包含任何方法)。
序列化的核心机制与实现步骤
核心类
ObjectOutputStream:将对象写入输出流(序列化)。ObjectInputStream:从输入流读取对象(反序列化)。
标准实现步骤
- 使类实现Serializable接口。
- 创建ObjectOutputStream对象,绑定文件/网络输出流。
- 调用writeObject()方法,将对象写入流。
- 关闭资源。
- 反序列化时:使用ObjectInputStream的readObject()读取。
注意:序列化会递归保存对象的所有成员变量(包括引用对象),除非被标记为transient。
简单对象序列化与反序列化
代码实现
import java.io.*;
// 1. 实现Serializable接口
class Employee implements Serializable {
private static final long serialVersionUID = 1L;
private String name;
private int age;
private double salary;
public Employee(String name, int age, double salary) {
this.name = name;
this.age = age;
this.salary = salary;
}
@Override
public String toString() {
return "Employee{name='" + name + "', age=" + age + ", salary=" + salary + "}";
}
}
public class SerializationDemo {
public static void main(String[] args) {
String filename = "employee.ser";
// 序列化
try (ObjectOutputStream oos = new ObjectOutputStream(
new FileOutputStream(filename))) {
Employee emp = new Employee("张三", 30, 5000.0);
oos.writeObject(emp);
System.out.println("序列化成功:" + emp);
} catch (IOException e) {
e.printStackTrace();
}
// 反序列化
try (ObjectInputStream ois = new ObjectInputStream(
new FileInputStream(filename))) {
Employee emp = (Employee) ois.readObject();
System.out.println("反序列化成功:" + emp);
} catch (IOException | ClassNotFoundException e) {
e.printStackTrace();
}
}
}
输出结果
序列化成功:Employee{name='张三', age=30, salary=5000.0}
反序列化成功:Employee{name='张三', age=30, salary=5000.0}
关键点:serialVersionUID用于版本控制,如果类结构发生变化(如增加字段),反序列化会抛出InvalidClassException。
自定义序列化与transient关键字
场景:密码不应被序列化
class User implements Serializable {
private static final long serialVersionUID = 1L;
private String username;
private transient String password; // 不会被序列化
private int age;
public User(String username, String password, int age) {
this.username = username;
this.password = password;
this.age = age;
}
// 自定义序列化逻辑:加密密码后才序列化
private void writeObject(ObjectOutputStream oos) throws IOException {
oos.defaultWriteObject();
// 这里简单模拟加密:将密码反转
String encrypted = new StringBuilder(password).reverse().toString();
oos.writeObject(encrypted);
}
private void readObject(ObjectInputStream ois) throws IOException, ClassNotFoundException {
ois.defaultReadObject();
String encrypted = (String) ois.readObject();
this.password = new StringBuilder(encrypted).reverse().toString();
}
@Override
public String toString() {
return "User{username='" + username + "', password='" + password + "', age=" + age + "}";
}
}
public class CustomSerialization {
public static void main(String[] args) {
String file = "user.ser";
User user = new User("admin", "123456", 25);
System.out.println("原始对象:" + user);
try (ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream(file))) {
oos.writeObject(user);
} catch (IOException e) { e.printStackTrace(); }
try (ObjectInputStream ois = new ObjectInputStream(new FileInputStream(file))) {
User loaded = (User) ois.readObject();
System.out.println("反序列化对象:" + loaded);
System.out.println("密码一致性:" + "123456".equals(loaded.password));
} catch (IOException | ClassNotFoundException e) { e.printStackTrace(); }
}
}
输出结果
原始对象:User{username='admin', password='123456', age=25}
反序列化对象:User{username='admin', password='123456', age=25}
密码一致性:true
说明:
transient字段不会被默认序列化,但可通过writeObject/readObject自定义序列化逻辑。- 注意:自定义方法必须是
private,且名称固定。
继承与序列化时的注意事项
场景:父类未实现Serializable
class Parent {
String parentField = "parent";
public Parent() {
System.out.println("Parent constructor called");
}
}
class Child extends Parent implements Serializable {
private static final long serialVersionUID = 1L;
String childField = "child";
public Child() {
System.out.println("Child constructor called");
}
}
public class InheritanceSerialization {
public static void main(String[] args) {
Child child = new Child();
child.parentField = "modified parent";
String file = "child.ser";
try (ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream(file))) {
oos.writeObject(child);
} catch (IOException e) { e.printStackTrace(); }
try (ObjectInputStream ois = new ObjectInputStream(new FileInputStream(file))) {
Child loaded = (Child) ois.readObject();
System.out.println("parentField: " + loaded.parentField);
System.out.println("childField: " + loaded.childField);
} catch (IOException | ClassNotFoundException e) { e.printStackTrace(); }
}
}
输出结果
Parent constructor called
Child constructor called
Parent constructor called // 反序列化时父类构造器被调用
parentField: parent // 父类字段被重置为默认值
childField: child
核心规则:
- 如果父类未实现
Serializable,则父类字段不会被序列化,反序列化时,父类的无参构造器会被调用,字段恢复为默认值。 - 如果父类实现了
Serializable,则子类自动可序列化,且所有字段都会被保存。
常见问题与最佳实践
什么时候序列化会失败?
- 类未实现
Serializable。 - 类包含了不可序列化的成员(可通过
transient解决)。 serialVersionUID不匹配。- 网络传输过程中流中断。
如何控制序列化内容?
- 使用
transient排除敏感字段。 - 实现
Externalizable接口(需手动实现writeExternal和readExternal,性能更高但更复杂)。
序列化与克隆的关系?
- 通过序列化实现深拷贝:将对象序列化到字节数组,再反序列化回来。
序列化版本号(serialVersionUID)的作用?
- 用于验证反序列化时类的版本是否兼容,建议显式声明,避免JVM自动生成(自动生成依赖于类结构,易变)。
问答环节:解决你最常见的疑惑
Q1:static字段会被序列化吗?
A:不会。static字段属于类级别,不属于对象实例,序列化只保存实例状态。
Q2:如果父类实现了Serializable,子类还需要实现吗? A:不需要,子类自动继承序列化能力,但需确保子类中所有成员都可序列化。
Q3:transient关键字能用在哪些地方? A:只能用于实例变量(成员变量),不能用于类变量(static)或局部变量。
Q4:反序列化时构造器会被调用吗?
A:对于实现了Serializable的类,不会调用任何构造器,对象通过字节流直接恢复状态,但如果是父类未实现Serializable,则父类的无参构造器会被调用(如上例所示)。
Q5:如何防止反序列化攻击? A:
- 使用
ObjectInputFilter设置白名单/黑名单(Java 9+)。 - 对敏感字段使用
transient。 - 实现
readObjectNoData等方法提供默认值。 - 避免反序列化不可信来源的数据。
Q6:序列化与JSON/XML的区别? A:JSON/XML是文本格式,跨语言、可读性好,但体积较大;Java序列化是二进制格式,依赖Java环境,但能完整保留对象类型和引用关系,实际开发中,RPC框架多使用JSON(如RESTful)或专用二进制协议(如Protobuf)替代Java序列化。
通过以上案例和解析,你应该能掌握Java序列化的核心用法、自定义技巧以及潜在陷阱,在实际项目中,优先选择成熟的序列化框架(如Jackson、Protostuff)以提升性能和安全性,如有更多问题,欢迎在评论区交流!