JAVA的序列化与反序列化

156 阅读7分钟

序列化和反序列化是将对象转换为字节流(序列化)和从字节流恢复为对象(反序列化)的过程。这在需要将对象持久化到文件、数据库,或通过网络传输对象时非常有用。让我们详细说明这两个过程,并通过一个简单的例子来说明。

一、序列化的过程

序列化将 Java 对象的状态转换为字节流,这样它可以被存储或传输。以下是序列化的具体步骤:

  • 步骤 1:对象实现 Serializable 接口。该接口是一个标记接口,没有任何方法,表示该对象可以被序列化。
  • 步骤 2:使用 ObjectOutputStream 将对象写入到输出流中。ObjectOutputStream 会将对象转换为字节流,并将其写入文件或网络中。
  • 步骤 3:在序列化过程中,Java 会记录对象的类信息(包括类的serialVersionUID)和对象的数据(字段的值)。
import java.io.*;

class Person implements Serializable {
    private static final long serialVersionUID = 1L;
    private String name;
    private int age;

    public Person(String name, int age) {
        this.name = name;
        this.age = age;
    }

    @Override
    public String toString() {
        return "Person{name='" + name + "', age=" + age + "}";
    }
}

public class SerializationExample {
    public static void main(String[] args) {
        Person person = new Person("Alice", 30);

        // 序列化过程
        try (ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("person.ser"))) {
            oos.writeObject(person);  // 将对象写入到文件中
            System.out.println("序列化完成: " + person);
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

在这个例子中,Person类实现了Serializable接口。在 main() 方法中,我们创建了一个 Person 对象,并使用 ObjectOutputStream 将它写入到文件 person.ser 中。

二、反序列化的过程

反序列化是将字节流恢复为 Java 对象的过程。具体步骤如下:

  • 步骤 1:使用 ObjectInputStream 从输入流中读取字节流。
  • 步骤 2ObjectInputStream 读取字节流并将其转换为对象。在这个过程中,Java 会检查字节流中记录的类信息(包括类名和serialVersionUID)是否与当前类匹配。
  • 步骤 3:如果类信息匹配,Java 会重新创建对象,并将字段值恢复到对象中。
import java.io.*;

public class DeserializationExample {
    public static void main(String[] args) {
        // 反序列化过程
        try (ObjectInputStream ois = new ObjectInputStream(new FileInputStream("person.ser"))) {
            Person person = (Person) ois.readObject();  // 从文件中读取对象
            System.out.println("反序列化完成: " + person);
        } catch (IOException | ClassNotFoundException e) {
            e.printStackTrace();
        }
    }
}

在这个例子中,ObjectInputStream 从文件 person.ser 中读取对象,并将其恢复为 Person 对象。恢复后的对象保留了原始对象的状态(nameage 字段的值)。

三、序列化与反序列化过程的详细描述

  • 序列化过程

    1. 类检查:Java 首先检查对象所属的类是否实现了 Serializable 接口。如果没有实现,会抛出 NotSerializableException
    2. 对象写入流ObjectOutputStream 将对象的类信息(类名、serialVersionUID)和对象的字段值(包括基本类型和引用类型)写入到输出流中。
    3. 写入serialVersionUID:Java 会将类的 serialVersionUID 写入到字节流中,以便在反序列化时验证类的版本。
    4. 对象的嵌套序列化:如果对象包含引用其他对象(例如嵌套对象),这些嵌套对象也会被递归地序列化。
  • 反序列化过程

    1. 读取类信息ObjectInputStream 从输入流中读取字节流,包括类的完全限定名和 serialVersionUID
    2. 类匹配:Java 会检查字节流中的类信息与当前的类是否匹配。如果类名或 serialVersionUID 不匹配,会抛出 InvalidClassException
    3. 恢复对象:如果类信息匹配,Java 会创建对象的实例,并将字段值从字节流中恢复到对象中。
    4. 嵌套对象恢复:如果对象包含引用其他对象(例如嵌套对象),这些对象也会被递归地反序列化。

四、序列化和反序列化中的注意事项

  • serialVersionUIDserialVersionUID 是序列化机制中用于验证类版本一致性的标识。如果类的结构发生变化且serialVersionUID不匹配,反序列化会失败。因此,在类可能发生变化的情况下,手动设置 serialVersionUID 是一种好习惯。
  • 对象的深度序列化:序列化会递归地序列化对象中所有引用的对象,因此所有被引用的对象也需要实现 Serializable 接口。
  • transient 关键字:如果某个字段不需要序列化,可以使用 transient 关键字标记,这样该字段在序列化过程中会被忽略。

五、总结

  • 序列化:将对象的状态转换为字节流,用于存储或传输。
  • 反序列化:将字节流恢复为对象,用于读取存储的数据或接收传输的对象。
  • serialVersionUID:用于验证类的版本是否一致,确保序列化和反序列化过程的兼容性。

六、题外话 - 防止反序列化破坏单例

下面是获取单例对象的一种方法

如果实现了序列化接口, 还要做什么来防止反序列化破坏单例

public final class Singleton implements Serializable {
    private Singleton() {}

    private static final Singleton INSTANCE = new Singleton();

    public static Singleton getInstance() {
        return INSTANCE;
    }

    public Object readResolve() {
        return INSTANCE;
    }
}

单例模式与序列化的挑战

单例模式旨在确保某个类只有一个实例,并提供全局访问点。问题在于,当单例对象被序列化和反序列化时,可能会出现以下情况:

  • 序列化: 对单例对象进行序列化时,实际上是将单例对象的状态写入到一个字节流中。
  • 反序列化: 当从字节流中反序列化对象时,Java 的默认行为是创建一个新的实例,而不是返回原来的单例实例。

这样,虽然初始的单例实现是正确的,但反序列化操作可能导致创建出新的对象实例,从而破坏了单例模式的唯一性。

readResolve() 方法的作用

为了解决这个问题,Java 提供了 readResolve() 方法。这是 Serializable 接口的一部分,允许类在反序列化时进行自定义的处理。

  • 定义: readResolve() 是一个特殊的方法,它的返回值会代替反序列化过程中的新创建对象的引用。
  • 作用: 当 Java 在反序列化过程中创建一个新的对象实例时,readResolve() 方法被调用,它返回单例类的唯一实例。这个返回的实例会替代新创建的对象,从而保证反序列化后的对象仍然是单例。

示例代码分析

在你的单例类中,readResolve() 方法的实现如下:

public Object readResolve() {
    return INSTANCE;
}

工作原理:

  1. 反序列化过程: 当一个序列化的单例对象被读取(反序列化)时,Java 会首先创建一个新的对象实例。
  2. 调用 readResolve() : 在创建新对象之后,Java 会调用 readResolve() 方法。
  3. 返回单例实例: readResolve() 方法返回 INSTANCE,这是单例类中唯一的实例。
  4. 替换新对象: 返回的 INSTANCE 替代了新创建的对象实例,因此最终得到的对象引用仍然是单例的唯一实例。

结果: 即使通过反序列化创建了一个新对象,readResolve() 方法确保了在反序列化后返回的对象是唯一的单例实例,从而保持了单例模式的正确性。

关键点总结

  • 序列化: 将对象的状态转换为字节流。
  • 反序列化: 从字节流中恢复对象状态,可能导致新的实例创建。
  • readResolve() 方法: 确保在反序列化过程中返回的是单例实例,防止创建新的对象实例。

例外情况和注意事项

  • 反射攻击: 即使 readResolve() 方法可以防止反序列化破坏单例,但如果不采取额外措施(如反射检查),仍然可能通过反射创建多个实例。
  • 实现细节: 在某些实现中,还需要确保类的构造函数和 readResolve() 方法能够有效地防止其他类型的攻击或不一致性。