Java 中的序列化和反序列化

2025 年 4 月 20 日 | 12 分钟阅读

Java 中的序列化是将对象的内存状态写入字节流的机制。它主要用于 Hibernate、RMI、JPA、EJB 和 JMS 技术。

序列化的反向操作称为反序列化,其中字节流被转换回对象。序列化和反序列化过程是独立于平台的。这意味着我们可以在一个平台上序列化一个对象,并在不同的平台上反序列化它。

为了序列化对象,我们调用 ObjectOutputStream 类的 writeObject() 方法;为了反序列化,我们调用 ObjectInputStream 类的 readObject() 方法。

Java 的序列化功能将对象转换为字节流,使其更易于存储或通过网络发送。许多不同的技术,包括 Hibernate、RMI(远程方法调用)、JPA(Java 持久化 API)、EJB(企业级 Java Beans)和 JMS(Java 消息服务),都大量使用了这种方法。

我们必须实现 Serializable 接口才能序列化对象。

Java 序列化的优点

它主要用于在网络上传输对象的内存状态(这被称为编组)。

java serialization
  1. 平台独立性:当在不同平台上运行的独立 Java 虚拟机 (JVM) 之间传输序列化对象时,没有兼容性问题。Java 序列化由于其平台独立性而成为系统间通信的有效技术。
  2. 网络通信:Java 序列化促进了对象数据通过网络的传输。这个过程,称为编组,允许对象被序列化成字节流并通过网络发送到另一台机器上进行重建。它对于涉及分布式系统的应用程序至关重要,例如客户端-服务器架构和 Web 服务。
  3. 对象持久性:序列化允许无限期地存储对象状态。通过使用序列化对象,可以将数据保存在程序执行之间,这些对象可以保存到磁盘或数据库中,然后进行检索。

java.io.Serializable 接口

Serializable 是一个标记接口(没有数据成员和方法)。它用于“标记”Java 类,以便这些类的对象可以获得某种功能。CloneableRemote 也是标记接口。

需要持久化其对象的类必须实现 Serializable 接口。

String 类和所有包装类默认实现了 java.io.Serializable 接口。

让我们看下面的例子

Student.java

说明

此代码定义了一个名为 Student 的 Java 类,实现了 Serializable 接口。Serializable 接口作为标记,表示 Student 类的实例可以序列化,这意味着它们的状态可以转换为字节流以进行存储或传输。

学生类有两个实例变量,id 和 name,分别表示学生的 ID 和姓名。它还包括一个构造函数,用于使用作为参数传递的值初始化这些变量。通过实现 Serializable,Student 类的实例可以无缝地序列化和反序列化,使其适用于对象持久性或网络通信等场景。

ObjectOutputStream 类

ObjectOutputStream 类用于将原始数据类型和 Java 对象写入 OutputStream。只有支持 java.io.Serializable 接口的对象才能写入流。

它充当字节流和 Java 对象之间的链接,允许将对象序列化并写入文件或网络连接等目标。重要的是要记住,只有实现了 java.io 的对象。ObjectOutputStream 可以用于将可序列化接口写入流中。此接口确保对象能够被序列化并将其状态转换为字节流。

构造函数

public ObjectOutputStream(OutputStream out) throws IOException它创建一个写入指定 OutputStream 的 ObjectOutputStream。
public ObjectOutputStream(OutputStream out, int bufferSize) throws IOException它创建一个写入指定 OutputStream 且具有给定缓冲区大小的 ObjectOutputStream。
protected ObjectOutputStream() throws IOException, SecurityException它用于子类构造。创建 ObjectOutputStream 实例。
protected ObjectOutputStream(OutputStream out) throws IOException它用于子类构造。创建一个写入指定 OutputStream 的 ObjectOutputStream。

重要方法

方法描述
public final void writeObject(Object obj) throws IOException {}它将指定的对象写入 ObjectOutputStream。
public void flush() throws IOException {}它刷新当前的输出流。
public void close() throws IOException {}它关闭当前的输出流。

ObjectInputStream 类

ObjectInputStream 反序列化使用 ObjectOutputStream 写入的对象和原始数据。

构造函数

1) public ObjectInputStream(InputStream in) throws IOException {}它创建一个从指定 InputStream 读取的 ObjectInputStream。

重要方法

方法描述
1) public final Object readObject() throws IOException, ClassNotFoundException{}它从输入流中读取一个对象。
2) public void close() throws IOException {}它关闭 ObjectInputStream。

Java 序列化示例

在这个例子中,我们将序列化上面代码中的 Student 类的对象。ObjectOutputStream 类的 writeObject() 方法提供了序列化对象的功能。我们将对象的内存状态保存到名为 f.txt 的文件中。

Persist.java

输出

success

说明

上述 Java 代码演示了如何使用 ObjectOutputStream 类进行对象序列化。在 main() 方法中,创建了一个 ID 为 211,姓名为“ravi”的 Student 类实例。接下来,使用生成的名为 fout 的 FileOutputStream 将字节写入名为“f.txt”的文件中。然后,通过创建一个名为 out 的 ObjectOutputStream 并包装 fout,将 Student 对象 s1 序列化并写入文件。

为了释放相关系统资源,在确保所有缓冲数据已发送到文件后,使用 flush() 将流关闭。如果成功,软件会在控制台打印“success”。

Java 反序列化示例

反序列化是从序列化状态重建对象的过程。它是序列化的逆操作。让我们看一个从反序列化对象读取数据的示例。

反序列化是从序列化状态重建对象的过程。它是序列化的逆操作。让我们看一个从反序列化对象读取数据的示例。

Depersist.java

输出

211 ravi

说明

此 Java 代码片段演示了使用 ObjectInputStream 类进行对象反序列化。在主程序中,一个从文件“f.txt”读取数据的 FileInputStream 被包装在一个名为 in 的 ObjectInputStream 中。使用此流读取之前写入文件的序列化对象。之后,使用 readObject() 函数反序列化对象并将其转换为 Student 类。

然后,反序列化的 Student 对象的 ID 和姓名以及其他相关数据会打印到控制台。为了释放相关系统资源,最终使用 close() 方法关闭流。在反序列化期间,捕获任何异常并显示其错误消息。

Java 序列化与继承(IS-A 关系)

如果一个类实现了 Serializable 接口,那么它的所有子类也将是可序列化的。让我们看下面的例子

SerializeISA.java

输出

success
211 ravi Engineering 50000

说明

所提供的 Java 代码展示了对象序列化和反序列化,并结合了继承。在代码中,Person 类作为超类,定义了 id 和 name 等基本属性,并实现了 Serializable 接口。Student 类继承自 Person,添加了 course 和 fee 等特定属性,并包含一个初始化所有属性(包括继承属性)的构造函数。

在 SerializeISA 类中,通过创建一个 Student 对象 (s1),使用 ObjectOutputStream 将其写入名为“f.txt”的文件,然后刷新并关闭流来演示序列化。随后进行反序列化,通过 ObjectInputStream 从“f.txt”读取序列化对象,并将其属性打印到控制台。实现了异常处理来管理文件操作或对象操作期间可能发生的错误。

Java 序列化与聚合(HAS-A 关系)

如果一个类引用了另一个类,则所有引用都必须是 Serializable,否则将不执行序列化过程。在这种情况下,运行时将抛出 NotSerializableException

Address.java

Student.java

由于 Address 不可序列化,因此您无法序列化 Student 类的实例。

注意:对象中的所有对象都必须是可序列化的。

说明

代码中提供了 Address 和 Student 两个类。Address 类由其构造函数初始化,包含地址的 addressLine、city 和 state 信息。Student 类通过表示具有 name 和 id 等属性的学生并提供对 Address 对象的引用来创建 HAS-A 关系。

为了允许 Student 实例被序列化,该类实现了 Serializable 接口。然而,Address 类中没有 Serializable 实现,这可能会导致序列化问题。为了解决这个问题,Address 类还应该实现 Serializable,这将保证带有 Address 引用的 Student 对象无缝地序列化。

Java 序列化与静态数据成员

如果一个类有任何静态数据成员,它们将不会被序列化。

换句话说,当一个类被序列化时,只有它的实例变量会被序列化;静态变量不会被序列化。静态变量不属于对象的内存状态,也不会随对象一起序列化,因为它们属于类本身,而不是类的任何特定实例。

因此,当对象被反序列化时,它的静态变量将使用它们的默认值或类加载到内存时初始化的值进行初始化,而不是从序列化流中初始化。这种行为旨在保持一致性并防止由于静态变量在类中所有实例之间的共享性质而可能出现的问题。

Employee.java

说明

在提供的 Employee 类中,有三个成员:id、name 和 company。id 和 name 字段表示每个员工的实例特定数据,在 Employee 对象序列化时将被序列化。然而,company 字段被声明为 static,使其成为 Employee 类的所有实例共享的类级别变量。静态变量不会被序列化,因为它们不属于对象的内存状态;它们属于类定义本身。

在序列化过程中,只有实例变量会被序列化,而静态变量会被忽略。因此,当 Employee 对象被序列化然后反序列化时,id 和 name 字段将恢复到它们之前的值,但 company 字段将保留其在类定义中的值,而不是来自序列化流。这种行为确保了静态变量在所有实例之间保持一致性,并且不受对象序列化的影响。

Java 序列化与数组或集合

规则:在数组或集合中,所有对象都必须是可序列化的。如果任何对象不可序列化,序列化将失败。

在 Java 序列化中使用数组或集合时,确保其中每个对象都可以序列化至关重要。这意味着数组或对象集合中的每个对象(例如 ArrayList)都需要实现 Serializable 接口。

如果数组或集合中的任何对象不可序列化,则尝试序列化数组或集合将失败,通常会抛出 NotSerializableException。这是因为序列化是一个递归操作,这意味着数组或集合中序列化的每个对象都会同样被序列化。

为避免在使用数组或集合时出现序列化失败,请确保其中存储的所有对象都是可序列化的。如果必须存储不可序列化的对象,请考虑使用瞬态字段或自定义序列化方法等替代方案来处理其序列化和反序列化。此外,始终优雅地处理序列化异常,以提供适当的错误处理和用户反馈。

Java 中的 Externalizable

Externalizable 接口提供了将对象状态写入压缩字节流的功能。它不是一个标记接口。

当一个类实现 Externalizable 接口时,它必须提供两个方法的实现:writeExternal() 和 readExternal()。这些方法由序列化机制调用,分别用于将对象的状态写入 ObjectOutput 流和从 ObjectInput 流读取对象的状态。

Externalizable 接口提供了两种方法

  • public void writeExternal(ObjectOutput out) throws IOException
  • public void readExternal(ObjectInput in) throws IOException

1. writeExternal(ObjectOutput out):此方法在序列化期间调用,负责将对象状态写入指定的 ObjectOutput 流。开发人员可以通过以任何所需格式将特定字段或数据写入流来定制序列化过程。它可以用于通过仅写入必要数据或压缩输出来优化序列化,如您解释中提到的。

2. readExternal(ObjectInput in):此方法在反序列化期间调用,负责从指定的 ObjectInput 流重建对象状态。开发人员必须以与序列化期间写入数据相同的顺序和格式从流中读取数据。它允许自定义反序列化逻辑,例如处理向后兼容性或对输入数据执行额外的验证检查。

Java transient 关键字

如果我们不想序列化类的任何数据成员,我们可以将其标记为 transient。

Employee.java

现在,id 将不会被序列化,因此当您在序列化后反序列化对象时,您将不会获得 id 的值。它将始终返回默认值。在这种情况下,它将返回 0,因为 id 的数据类型是整数。

访问下一页了解更多详情。

SerialVersionUID

运行时序列化过程会为每个 Serializable 类关联一个 ID,称为 SerialVersionUID。它用于验证序列化对象的发送方和接收方。发送方和接收方必须是相同的。为了验证它,使用了 SerialVersionUID。发送方和接收方必须具有相同的 SerialVersionUID,否则,当我们反序列化对象时,将抛出 InvalidClassException。我们还可以在 Serializable 类中声明自己的 SerialVersionUID。为此,我们需要创建一个字段 SerialVersionUID 并为其赋值。它必须是 long 类型,并且是 static 和 final。建议在类中显式声明 serialVersionUID 字段,并将其设为 private。

如果 serialVersionUID 值匹配,则反序列化操作顺利进行。如果它们不匹配,则会引发 InvalidClassException,从而停止对象的反序列化,这表明发送方和接收方之间的类结构可能存在差异。此方法可以防止由于类版本不匹配而导致意外行为,并有助于维护数据完整性。

建议直接在可序列化类中定义一个静态 final serialVersionUID 字段,尽管 Java 会根据不同的变量(如类结构、字段类型和方法签名)自动为可序列化类生成一个 serialVersionUID。开发人员可以通过显式定义 serialVersionUID 来保证一致的序列化行为,即使在类签名发生变化(例如,添加或删除新字段)的情况下也是如此。

例如

现在,Serializable 类将如下所示

Employee.java

说明

所提供的 Employee 类实现了 Serializable 接口,表明该类的实例可以被序列化。在类中声明了一个名为 serialVersionUID 的静态 final 变量,它作为序列化的版本控制机制。

此字段确保类的序列化形式在不同版本之间保持一致。开发人员可以通过明确设置 serialVersionUID 的值(在本例中为 1L)来更好地控制版本控制并保持对序列化过程的控制。

Employee 类中有两个实例变量,name 和 id,它们分别代表员工的姓名和识别号。当 Employee 对象被序列化时,这些变量也将随对象的状态一起序列化。