在Java中实现单例设计模式的有效方法是什么?


当前回答

另一个经常用来反对单态的论点是它们的可测试性问题。出于测试目的,单体不容易被模拟。如果这是一个问题,我想做以下轻微修改:

public class SingletonImpl {

    private static SingletonImpl instance;

    public static SingletonImpl getInstance() {
        if (instance == null) {
            instance = new SingletonImpl();
        }
        return instance;
    }

    public static void setInstance(SingletonImpl impl) {
        instance = impl;
    }

    public void a() {
        System.out.println("Default Method");
    }
}

添加的setInstance方法允许在测试期间设置单例类的实体模型实现:

public class SingletonMock extends SingletonImpl {

    @Override
    public void a() {
        System.out.println("Mock Method");
    }

}

这也适用于早期初始化方法:

public class SingletonImpl {

    private static final SingletonImpl instance = new SingletonImpl();

    private static SingletonImpl alt;

    public static void setInstance(SingletonImpl inst) {
        alt = inst;
    }

    public static SingletonImpl getInstance() {
        if (alt != null) {
            return alt;
        }
        return instance;
    }

    public void a() {
        System.out.println("Default Method");
    }
}

public class SingletonMock extends SingletonImpl {

    @Override
    public void a() {
        System.out.println("Mock Method");
    }

}

这有一个缺点,就是将此功能也暴露给普通应用程序。其他编写该代码的开发人员可能会尝试使用“setInstance”方法来更改特定函数,从而改变整个应用程序的行为,因此该方法的javadoc中至少应该包含一个良好的警告。

尽管如此,对于模型测试的可能性(当需要时),这种代码暴露可能是可以接受的代价。

其他回答

最简单的单例类:

public class Singleton {
  private static Singleton singleInstance = new Singleton();
  private Singleton() {}
  public static Singleton getSingleInstance() {
    return singleInstance;
  }
}

根据用法,有几个“正确”答案。

自Java5以来,最好的方法是使用枚举:

public enum Foo {
   INSTANCE;
}

在Java 5之前,最简单的情况是:

public final class Foo {

    private static final Foo INSTANCE = new Foo();

    private Foo() {
        if (INSTANCE != null) {
            throw new IllegalStateException("Already instantiated");
        }
    }

    public static Foo getInstance() {
        return INSTANCE;
    }

    public Object clone() throws CloneNotSupportedException{
        throw new CloneNotSupportedException("Cannot clone instance of this class");
    }
}

让我们复习一下代码。首先,你希望这门课是期末考试。在本例中,我使用了final关键字来让用户知道它是final。然后,需要将构造函数设为私有,以防止用户创建自己的Foo。从构造函数中抛出异常会阻止用户使用反射来创建第二个Foo。然后创建一个私有的静态final Foo字段来保存唯一的实例,并创建一个公共的静态Foo-getInstance()方法来返回它。Java规范确保只有在首次使用类时才调用构造函数。

当您有一个非常大的对象或大量的构造代码,并且还有其他可访问的静态方法或字段,这些方法或字段可能在需要实例之前使用,那么只有在需要时才需要使用惰性初始化。

您可以使用私有静态类来加载实例。代码将如下所示:

public final class Foo {

    private static class FooLoader {
        private static final Foo INSTANCE = new Foo();
    }

    private Foo() {
        if (FooLoader.INSTANCE != null) {
            throw new IllegalStateException("Already instantiated");
        }
    }

    public static Foo getInstance() {
        return FooLoader.INSTANCE;
    }
}

由于行private static final Foo INSTANCE=new Foo();仅在实际使用FooLoader类时执行,这会处理延迟实例化,并保证线程安全。

当您还希望能够序列化对象时,需要确保反序列化不会创建副本。

public final class Foo implements Serializable {

    private static final long serialVersionUID = 1L;

    private static class FooLoader {
        private static final Foo INSTANCE = new Foo();
    }

    private Foo() {
        if (FooLoader.INSTANCE != null) {
            throw new IllegalStateException("Already instantiated");
        }
    }

    public static Foo getInstance() {
        return FooLoader.INSTANCE;
    }

    @SuppressWarnings("unused")
    private Foo readResolve() {
        return FooLoader.INSTANCE;
    }
}

方法readResolve()将确保返回唯一的实例,即使对象是在程序的上一次运行中序列化的。

在写它之前,真的要考虑一下为什么你需要一个单例。关于使用它们有一个准宗教的争论,如果你在Java中搜索单例,你很容易就会发现。

就我个人而言,出于许多原因,我尽量避免单身汉,其中大部分可以通过谷歌搜索单身汉找到。我觉得单身汉经常被虐待,因为他们很容易被每个人理解。它们被用作将“全局”数据引入OO设计的机制,因为它很容易绕过对象生命周期管理(或者真正思考如何从B内部实现a)。看看像控制反转(IoC)或依赖注入(DI)这样的事情,可以找到一个不错的中间地带。

如果你真的需要一个,那么Wikipedia有一个很好的例子来说明单例的正确实现。

我对一些答案感到困惑,这些答案建议依赖注入(DI)作为使用单态的替代方案;这些都是不相关的概念。您可以使用DI注入单例或非单例(例如,每个线程)实例。至少如果您使用Spring2.x,这是正确的,我不能为其他DI框架说话。

所以我对OP的回答是(除了最简单的示例代码之外):

使用类似Spring framework的DI框架,然后将其作为DI配置的一部分,无论依赖项是单体的、请求范围的、会话范围的还是其他。

这种方法为您提供了一个很好的解耦(因此是灵活和可测试的)架构,其中是否使用单例是一个容易可逆的实现细节(当然,前提是您使用的任何单例都是线程安全的)。

忘记延迟初始化;这太有问题了。这是最简单的解决方案:

public class A {    

    private static final A INSTANCE = new A();

    private A() {}

    public static A getInstance() {
        return INSTANCE;
    }
}