1. 概述

在这个快速教程中,我们将学习如何在Java中创建可序列化的单例类。

2. 什么是序列化?

序列化是将Java对象的状态转换为可以存储在文件或数据库中的字节流的过程:

反序列化则是相反的过程,它根据字节流创建对象:

3. Serializable 接口

Serializable 接口是一个标记接口,也称为标签接口。标记接口为编译器和JVM提供运行时对象类型的元数据信息,但其中不包含任何字段、方法或常量。因此,实现它的类无需实现任何方法。

如果一个类实现了Serializable接口,其实例就可以被序列化或反序列化。

4. 什么是单例类?

在面向对象编程中,单例类是指一次只能有一个实例存在的类。第一次实例化后,再次尝试实例化该单例类会返回第一次创建的相同实例。以下是一个实现Serializable接口的单例类:

public class Singleton implements Serializable {

    private static Singleton INSTANCE;
    private String state = "State Zero";

    private Singleton() {}
    
    public static Singleton getInstance() {
        if (INSTANCE == null) {
            INSTANCE = new Singleton();
        }
        
        return INSTANCE;
    }

    public String getState() {
        return state;
    }

    public void setState(String state) {
        this.state = state;
    }
}

我们可以看到它有两个私有字段:INSTANCEstateINSTANCE是单例类的唯一实例,而state是一个字符串变量,用于保存类的状态。

5. 创建可序列化的单例类

问题在于,当我们实例化一个实现了Serializable的单例类,然后对其进行序列化和反序列化后,会得到两个单例类的实例,这违背了单例的特性:

@Test
public void givenSingleton_whenSerializedAndDeserialized_thenStatePreserved() {
    Singleton s1 = Singleton.getInstance();
    s1.setState("State One");

    try (
      FileOutputStream fos = new FileOutputStream("singleton_test.txt");
      ObjectOutputStream oos = new ObjectOutputStream(fos);
      FileInputStream fis = new FileInputStream("singleton_test.txt");
      ObjectInputStream ois = new ObjectInputStream(fis)) {

        // Serializing.
        oos.writeObject(s1);

        // Deserializing.
        Singleton s2 = (Singleton) ois.readObject();

        // Checking if the state is preserved.
        assertEquals(s1.getState(), s2.getState());

        // Checking if s1 and s2 are not the same instance.
        assertNotEquals(s1, s2);

    } catch (Exception e) {
        // ...
    }
}

上述测试代码通过了。尽管序列化和反序列化过程中状态得以保留,但新变量s2并未指向s1相同的实例,因此产生了两个Singleton类实例,这是不理想的。

为了创建可序列化的单例类,我们应该使用枚举单例模式:

public enum EnumSingleton {

    INSTANCE("State Zero");

    private String state;

    private EnumSingleton(String state) {
        this.state = state;
    }

    public EnumSingleton getInstance() {
        return INSTANCE;
    }

    public String getState() { 
        return this.state; 
    }

    public void setState(String state) { 
        this.state = state; 
    }
}

现在让我们看看当进行序列化和反序列化时会发生什么:

@Test
public void givenEnumSingleton_whenSerializedAndDeserialized_thenStatePreserved() {
    EnumSingleton es1 = EnumSingleton.getInstance();
    es1.setState("State One");

    try (
      FileOutputStream fos = new FileOutputStream("enum_singleton_test.txt");
      ObjectOutputStream oos = new ObjectOutputStream(fos);
      FileInputStream fis = new FileInputStream("enum_singleton_test.txt");
      ObjectInputStream ois = new ObjectInputStream(fis)) {
        
        // Serializing.
        oos.writeObject(es1);

        // Deserializing.
        EnumSingleton es2 = (EnumSingleton) ois.readObject();

        // Checking if the state is preserved.
        assertEquals(es1.getState(), es2.getState());

        // Checking if es1 and es2 are pointing to 
        // the same instance in memory.
        assertEquals(es1, es2);

    } catch (Exception e) {
        // ...
    }
}

上述测试代码也通过了。序列化和反序列化后,状态得以保留,变量es1es2都指向了最初创建的那个实例。

6. 总结

在这篇教程中,我们了解了如何在Java中创建可序列化的单例类。

如往常一样,完整的代码示例可在GitHub上找到。


» 下一篇: Java Weekly, 第466期