创建和销毁对象:使用私有构造函数还是枚举类型来创建单例?

单例:是一个只实例化一次的类。单例通常用来表示无状态对象,比如函数或系统组件,它们在本质上是唯一的。

实现单例有两种常见的方法。两者都基于保证构造函数私有,然后暴露一个公共静态成员以提供对唯一实例的访问。

方式1:公共字段的方式

一种方式,便是用final修饰成员变量:

// Singleton with public final field
public class Elvis {
    public static final Elvis INSTANCE = new Elvis();
    private Elvis() { ... }
    public void leaveTheBuilding() { ... }
}

在这种方式中,私有构造函数只调用一次,用于初始化public static final修饰的Elvis类型字段INSTANCE。不使用publicprotected修饰的构造函数保证了全局唯一性:一旦初始化了Elvis类,就会存在一个且只有一个Elvis实例,不多也不少。客户端所做的任何事情都不能改变这一点,但有一点需要注意:拥有特殊权限的客户端可以借助AccessibleObject.setAccessible方法利用反射调用私有构造函数。如果需要防范这种攻击,请修改构造函数,使其在请求创建第二个实例时抛出异常。

使用AccessibleObject.setAccessible方法调用私有构造函数示例:

Constructor<?>[] constructors = Elvis.class.getDeclaredConstructors();
AccessibleObject.setAccessible(constructors, true);

Arrays.stream(constructors).forEach(name -> {
    if (name.toString().contains("Elvis")) {
        Elvis instance = (Elvis) name.newInstance();
        instance.leaveTheBuilding();
    }
});

方式2:公共静态方法的方式

实现单例的另一种方式是,提供一个公共的静态工厂方法:

// Singleton with static factory
public class Elvis {
    private static final Elvis INSTANCE = new Elvis();
    private Elvis() { ... }
    public static Elvis getInstance() { return INSTANCE; }
    public void leaveTheBuilding() { ... }
}

所有对getInstance()方法的调用都返回相同的对象引用,并且不会创建其他Elvis实例(注意前面提到的相同警告)。

公共字段方式和公共静态方法方式的比较:

公共字段方式主要优点是API明确了类是单例的:public static修饰的字段是final的,因此它总是包含相同的对象引用。第二个优点是更简单。

静态工厂方法的一个优点是,它可以在不更改 API 的情况下决定类是否是单例。工厂方法返回唯一的实例,但是可以对其进行修改,为调用它的每个线程返回一个单独的实例。第二个优点是,如果应用程序需要的话,可以编写泛型的单例工厂。使用静态工厂的最后一个优点是方法引用能够作为一个提供者,例如 Elvis::getInstanceSupplier<Elvis> 的提供者。除非能够与这些优点沾边,否则使用 public 字段的方式更可取。

关于序列化: 上面的两种单例的实现方式,如果想让单例类实现可序列化,仅仅在声明中实现serializable是不行的。想要保证单例,应声明所有实例字段为transient,并提供readResolve 方法。否则,每次反序列化实例时,都会创建一个新实例,在我们的示例中,这会导致出现虚假的Elvis。为了防止这种情况发生,将这个readResolve 方法添加到 Elvis 类中:

// readResolve method to preserve singleton property
private Object readResolve() {
    // Return the one true Elvis and let the garbage collector
    // take care of the Elvis impersonator.
    return INSTANCE;
}

方式3:声明一个单元素枚举

// Enum singleton - the preferred approach
public enum Elvis {
    INSTANCE;
    public void leaveTheBuilding() { ... }
}

这种方法类似于 public 字段方法,但是它更简洁,默认提供了序列化机制,提供了对多个实例化的严格保证,即使面对复杂的序列化或反射攻击也是如此。这种方法可能有点不自然,但是单元素枚举类型通常是实现单例的最佳方法。 注意,如果你的单例必须扩展一个超类而不是 Enum(尽管你可以声明一个 Enum 来实现接口),你就不能使用这种方法。

References

  1. 创建和销毁对象:使用私有构造函数或枚举类型实施单例属性
赞(0)
未经允许禁止转载:四个空格 » 创建和销毁对象:使用私有构造函数还是枚举类型来创建单例?

评论 抢沙发

  • 昵称 (必填)
  • 邮箱 (必填)
  • 网址