【问题标题】:Implementing Singleton with an Enum (in Java)使用枚举实现单例(Java 中)
【发布时间】:2014-12-04 19:16:00
【问题描述】:

我读到可以使用 Enum 在 Java 中实现 Singleton,例如:

public enum MySingleton {
     INSTANCE;   
}

但是,以上是如何工作的?具体来说,Object 必须被实例化。在这里,MySingleton 是如何被实例化的?谁在做new MySingleton()

【问题讨论】:

  • 谁在做 new MySingleton() JVM 是。
  • INSTANCEpublic static final MySingleton INSTANCE = new MySingleton(); 相同。
  • ENUM - 保证单身。
  • 阅读dzone.com/articles/java-singletons-using-enum,为什么要使用枚举而不是其他方法。简短:使用序列化和反射时会出现问题。

标签: java design-patterns enums singleton


【解决方案1】:

这个,

public enum MySingleton {
  INSTANCE;   
}

有一个隐式的空构造函数。改为明确,

public enum MySingleton {
    INSTANCE;
    private MySingleton() {
        System.out.println("Here");
    }
}

如果您随后使用 main() 方法添加了另一个类,例如

public static void main(String[] args) {
    System.out.println(MySingleton.INSTANCE);
}

你会看到

Here
INSTANCE

enum 字段是编译时常量,但它们是 enum 类型的实例。而且,它们是在第一次引用枚举类型时构造的。

【讨论】:

  • 您应该补充一点,默认情况下枚举具有隐式私有构造函数,并且不需要显式添加私有构造函数,除非您实际上有需要在该构造函数中运行的代码
  • 每个枚举字段只创建​​一次实例,因此无需创建私有构造函数。
  • 公共枚举 MySingleton { INSTANCE,INSTANCE1; } 然后 System.out.println(MySingleton.INSTANCE.hashCode()); System.out.println(MySingleton.INSTANCE1.hashCode());它打印不同的哈希码。这是否意味着创建了两个 MySingleton 对象?
  • @scottmiles 是的。因为你有两个实例。根据定义,单身人士有一个。
  • private 修饰符对于 enum 构造函数没有意义,完全是多余的。
【解决方案2】:

enum 类型是 class 的特殊类型。

你的enum实际上会被编译成类似的东西

public final class MySingleton {
    public final static MySingleton INSTANCE = new MySingleton();
    private MySingleton(){} 
}

当您的代码第一次访问INSTANCE 时,类MySingleton 将被JVM 加载和初始化。此过程初始化 once 上方的 static 字段(懒惰地)。

【讨论】:

    【解决方案3】:

    在 Joshua Bloch 的 Java best practices book 中,您可以找到解释为什么应该使用私有构造函数或 Enum 类型来强制执行 Singleton 属性。这一章比较长,总结一下吧:

    将一个类设为 Singleton 会使测试其客户端变得困难,因为除非它实现了一个用作其类型的接口,否则不可能用模拟实现代替单例。 推荐的方法是通过简单地使用一个元素创建一个枚举类型来实现单例:

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

    此方法在功能上等同于公共字段方法,不同之处在于它 更简洁,免费提供序列化机制,并提供 对多次实例化的铁定保证,即使面对复杂的 序列化或反射攻击。

    虽然这种方法尚未广泛应用 采用单元素枚举类型是实现单例的最佳方式。

    【讨论】:

    • 我认为,要使单例可测试,您可以使用策略模式并让单例的所有操作都通过该策略。所以你可以有一个“生产”策略和一个“测试”策略......
    【解决方案4】:

    像所有枚举实例一样,Java 在加载类时实例化每个对象,并保证每个 JVM 只实例化一次。将INSTANCE 声明视为公共静态最终字段:Java 将在第一次引用该类时实例化该对象。

    实例是在静态初始化期间创建的,在Java Language Specification, section 12.4 中定义。

    不管怎样,Joshua Bloch 将此模式详细描述为Effective Java Second Edition 的第 3 项。

    【讨论】:

      【解决方案5】:

      正如之前在某种程度上提到的,枚举是一个 Java 类,它的定义必须以至少一个“枚举常量”开头。

      除此之外,枚举不能扩展或用于扩展其他类,枚举与任何类一样是一个类,您可以通过在常量定义下方添加方法来使用它:

      public enum MySingleton {
          INSTANCE;
      
          public void doSomething() { ... }
      
          public synchronized String getSomething() { return something; }
      
          private String something;
      }
      

      您可以按照以下方式访问单例的方法:

      MySingleton.INSTANCE.doSomething();
      String something = MySingleton.INSTANCE.getSomething();
      

      正如其他答案中提到的那样,使用枚举而不是类,主要是关于单例的线程安全实例化,并保证它始终只是一个副本。

      也许,最重要的是,这种行为是由 JVM 本身和 Java 规范保证的。

      这是Java specification 中关于如何防止枚举实例的多个实例的部分:

      枚举类型除了由其枚举常量定义的实例外,没有其他实例。尝试显式实例化枚举类型是编译时错误。 Enum 中的 final clone 方法确保永远不会克隆枚举常量,而序列化机制的特殊处理确保永远不会由于反序列化而创建重复的实例。禁止枚举类型的反射实例化。这四件事一起确保枚举类型的实例不存在超出枚举常量定义的实例。

      值得注意的是,在实例化之后,任何线程安全问题都必须像在任何其他类中一样使用 synchronized 关键字等处理。

      【讨论】:

        【解决方案6】:

        由于单例模式是关于拥有一个私有构造函数并调用一些方法来控制实例化(如一些getInstance),因此在枚举中我们已经有了一个隐式私有构造函数。

        我不完全知道 JVM 或某些 容器 是如何控制我们的 Enums 的实例的,但它似乎已经使用了隐式的 Singleton Pattern,不同的是我们不调用getInstance,我们只是调用枚举。

        【讨论】: