Skip to content

Java 序列化

Java 对象序列化是将对象的当前状态转换为字节序列的过程。此字节流包含对象的数据、其类型信息以及其中存储的数据类型。反序列化是逆过程:从字节流在内存中重新创建对象。

  • 将对象状态持久化到文件或数据库。
  • 通过网络传输对象(例如,在远程方法调用 - RMI 中)。
  • 在不同的 Java 虚拟机 (JVM) 之间分发对象。

一个关键特性是平台独立性:对象可以在一个平台(例如 Windows)上序列化,并在另一个平台(例如 Linux)上反序列化,前提是两个平台都可用的类定义。

核心类:

  • java.io.ObjectOutputStream: 将序列化对象写入 OutputStream(如 FileOutputStream)。关键方法:writeObject(Object obj)。
  • java.io.ObjectInputStream: 从 InputStream(如 FileInputStream)读取序列化对象。关键方法:readObject(),它返回一个 Object(需要强制类型转换)。

要使一个类能够进行序列化,它必须实现 java.io.Serializable 标记接口。此接口没有方法;它只是向 JVM 发出信号,表明该类的对象可以被序列化。

成功序列化的条件:

  • 类必须实现 java.io.Serializable(或其父类必须实现)。
  • 类中所有非静态、非 transient(瞬态)的字段必须是基本类型或它们本身是可序列化的。如果某个字段的类型不可序列化,则该字段必须标记为 transient。

使用 transient 关键字来表明某个字段不应包含在序列化过程中。这对于敏感数据(如密码)、计算得出的字段或引用非可序列化资源的字段非常有用。

在反序列化期间,JVM 会检查加载类的 serialVersionUID 是否与存储在序列化对象数据中的 serialVersionUID 匹配。这确保了用于序列化的类版本与用于反序列化的类版本之间的兼容性。

强烈建议在每个 Serializable 类中显式声明一个 private static final long serialVersionUID。如果省略,编译器会根据类结构生成一个,这可能会意外更改,从而导致在反序列化期间出现 InvalidClassException,如果类发生演变。

private static final long serialVersionUID = 1L; // 显式 UID 示例

示例:序列化和反序列化 Employee 对象

Section titled “示例:序列化和反序列化 Employee 对象”

考虑这个 Employee 类:

import java.io.Serializable;
public class Employee implements Serializable {
// 用于版本控制的显式 serialVersionUID
private static final long serialVersionUID = 1L;
public String name;
public String address;
// SSN 标记为 transient - 不会被序列化
public transient int SSN;
public int number;
// 默认构造函数 (通常需要用于反序列化框架,但对于基本的 Java 序列化并非严格必要)
public Employee() {}
public Employee(String name, String address, int SSN, int number) {
this.name = name;
this.address = address;
this.SSN = SSN;
this.number = number;
}
public void mailCheck() {
System.out.println("Mailing a check to " + name + " at " + address);
}
@Override
public String toString() {
return "Employee [name=" + name + ", address=" + address + ", SSN(transient)=" + SSN + ", number=" + number + "]";
}
}

此程序创建一个 Employee 对象,并使用 ObjectOutputStream 和 try-with-resources 将其状态保存到名为 employee.ser 的文件中。

import java.io.*;
public class SerializeDemo {
public static void main(String[] args) {
Employee e = new Employee("Reyan Ali", "Phokka Kuan, Ambehta Peer", 11122333, 101);
String filename = "employee.ser"; // 使用 .ser 约定
// 使用 try-with-resources 确保流被关闭
try (FileOutputStream fileOut = new FileOutputStream(filename);
ObjectOutputStream out = new ObjectOutputStream(fileOut)) {
out.writeObject(e); // 序列化对象
System.out.println("Serialized data is saved in " + filename);
} catch (IOException i) {
System.err.println("Serialization error: " + i.getMessage());
i.printStackTrace();
}
}
}

此程序读取 employee.ser 文件,并使用 ObjectInputStream 和 try-with-resources 重建 Employee 对象。

import java.io.*;
public class DeserializeDemo {
public static void main(String[] args) {
Employee e = null;
String filename = "employee.ser";
// 为输入流使用 try-with-resources
try (FileInputStream fileIn = new FileInputStream(filename);
ObjectInputStream in = new ObjectInputStream(fileIn)) {
// 反序列化并强制转换对象
e = (Employee) in.readObject();
} catch (IOException i) {
System.err.println("Deserialization IO error: " + i.getMessage());
i.printStackTrace();
return;
} catch (ClassNotFoundException c) {
System.err.println("反序列化期间未找到 Employee 类。");
c.printStackTrace();
return;
}
System.out.println("Deserialized Employee...");
System.out.println(e); // 使用 toString() 方法
// 演示 transient 字段的行为
System.out.println("Deserialized SSN (transient): " + e.SSN);
System.out.println("Number: " + e.number);
e.mailCheck();
}
}

组合输出(先序列化后反序列化):

Serialized data is saved in employee.ser
Deserialized Employee...
Employee [name=Reyan Ali, address=Phokka Kuan, Ambehta Peer, SSN(transient)=0, number=101]
Deserialized SSN (transient): 0
Number: 101
Mailing a check to Reyan Ali at Phokka Kuan, Ambehta Peer

关键观察:

  • try-with-resources 语句简化了资源管理,自动关闭流。
  • readObject() 方法可能会抛出 ClassNotFoundException,如果 JVM 找不到相应的类定义。
  • readObject() 的返回值是 Object,需要强制类型转换为期望的类型 (Employee)。
  • transient 字段 SSN 在序列化期间未保存,因此反序列化后其值默认为 0(int 的默认值)。

从不受信任的来源反序列化数据会带来重大的安全风险(反序列化漏洞 - Deserialization Vulnerabilities)。恶意构造的字节流可能导致任意代码执行。对于数据交换,特别是在跨越信任边界时,请考虑使用更安全的替代方案:

  • JSON: 使用 Jackson 或 Gson 等库将对象序列化/反序列化为 JSON 格式。更易于人类阅读,并且通常更安全地解析。
  • Protocol Buffers (Protobuf) / FlatBuffers: 基于模式的二进制格式,设计用于提高效率和跨语言兼容性。
  • 数据库持久化 (Database Persistence): 使用 JPA (Java Persistence API) 或 JDBC 将对象数据存储在关系型或 NoSQL 数据库中。

如果您必须使用 Java 序列化,请考虑在反序列化期间实现对象验证(例如,仔细重写 readObject)或使用过滤机制(ObjectInputFilter)。