Java中带参数的单例模式

168

我在维基百科上阅读了“单例模式”文章,发现了这个例子:

public class Singleton {
    // Private constructor prevents instantiation from other classes
    private Singleton() {}

    /**
     * SingletonHolder is loaded on the first execution of Singleton.getInstance() 
     * or the first access to SingletonHolder.INSTANCE, not before.
     */
    private static class SingletonHolder { 
        private static final Singleton INSTANCE = new Singleton();
    }

    public static Singleton getInstance() {
        return SingletonHolder.INSTANCE;
    }
}

虽然我非常喜欢这个单例模式的行为方式,但我不知道如何将构造函数参数集成进去。在Java中实现这个功能的首选方法是什么?我需要像下面这样做吗?

public class Singleton
{
    private static Singleton singleton = null;  
    private final int x;

    private Singleton(int x) {
        this.x = x;
    }

    public synchronized static Singleton getInstance(int x) {
        if(singleton == null) singleton = new Singleton(x);
        return singleton;
    }
}

谢谢!


编辑:我认为我的愿望使用 Singleton 开始引起了争议风暴。让我解释一下我的动机,希望有人能提供更好的建议。 我正在使用一个网格计算框架来并行执行任务。一般来说,我有像这样的东西:

// AbstractTask implements Serializable
public class Task extends AbstractTask
{
    private final ReferenceToReallyBigObject object;

    public Task(ReferenceToReallyBigObject object)
    {
        this.object = object;
    }

    public void run()
    {
        // Do some stuff with the object (which is immutable).
    }
}

即使我只是将我的数据的引用传递给所有任务,但当任务被序列化时,数据会被反复复制。我想做的是在所有任务之间共享对象。自然地,我可以像这样修改类:

// AbstractTask implements Serializable
public class Task extends AbstractTask
{
    private static ReferenceToReallyBigObject object = null;

    private final String filePath;

    public Task(String filePath)
    {
        this.filePath = filePath;
    }

    public void run()
    {
        synchronized(this)
        {
            if(object == null)
            {
                ObjectReader reader = new ObjectReader(filePath);
                object = reader.read();
            }
        }

        // Do some stuff with the object (which is immutable).
    }
}

正如你所看到的,即使在这里,我也面临着传递不同文件路径后什么都不起作用的问题。 这就是为什么我喜欢答案中发布的store(存储)的想法。 无论如何,我希望将加载文件的逻辑抽象成一个 Singleton 类,而不是在运行方法中包含该逻辑。 我不会提供另一个示例,但我希望你能理解我的想法。 请告诉我你更优雅的实现方法。 再次感谢!


1
工厂模式是您想要的东西。理想情况下,网格任务应该完全独立于其他任何内容,并获取发送所有执行和返回结果所需的数据。然而,这并不总是最可行的解决方案,因此将数据序列化到文件中并不是一个太糟糕的想法。我认为整个单例模式有点离题,您不需要一个单例。 - oxbow_lakes
2
很不幸,您使用了一个带有负面含义的Singleton术语。实际上,这种模式的正确术语是Interning。Interning是一种确保抽象值仅由一个实例表示的方法。字符串interning是最常见的用法:en.wikipedia.org/wiki/String_intern_pool。 - notnoop
您可能想看一下 Terracotta。它可以在集群中维护对象标识。当您发送到已经在集群中的数据的引用时,它不会被重新序列化。 - Taylor Gautier
23
暂且不论单例模式是否应该使用,我想指出这里几乎每个答案都假定提供参数的目的是为了创建“多个具有不同值参数”的单例。然而,另一个可能的目的是为了提供对外部对象的访问,这个外部对象是单例类的唯一实例永远需要的。因此,我们需要区分提供访问的参数和用于创建“多个单例实例”的参数。 - Carl
3
“带参数的单例模式”另一个应用场景是:一个 Web 应用程序将根据第一个请求(线程)传递的信息构建其唯一的不可变单例。请求的领域可以决定某些单例的行为,例如: - fustaki
我有一个不同的需求,因为我想为每个给定的参数拥有一个唯一的实例。 - Bionix1441
21个回答

202

我的观点很明确:带有参数的单例不是单例

按照定义,单例是一个你不想被实例化超过一次的对象。如果你试图向构造函数传递参数,那么单例的意义何在?

你有两个选择。如果你想要用一些数据初始化你的单例,你可以在实例化后加载它的数据,像这样:

SingletonObj singleton = SingletonObj.getInstance();
singleton.init(paramA, paramB); // init the object with data

如果你的单例正在执行重复的操作,并且每次使用不同的参数,那么最好将这些参数传递给正在执行的主方法:

SingletonObj singleton = SingletonObj.getInstance();
singleton.doSomething(paramA, paramB); // pass parameters on execution

无论如何,实例化永远不会带有参数。否则你的单例模式就不是单例模式了。


175
对不起,那不是真的。在某些情况下,您必须传递动态创建的参数,这些参数在整个应用程序运行时保持不变。因此,您不能在单例中使用常量,而必须在创建单例时传递该常量。传递一次后,它将成为整个时间段的相同常量。如果您需要在构造函数中使用特定常量,则无法使用 setter 方法。 - omni
76
如果你的应用程序在整个生命周期中只需要一个类实例,但是你需要在启动时提供该实例的值,为什么这不再是单例模式? - Oscar
5
反驳你的假设的一个例子是 Android 中的数据库辅助类。最佳实践是为该类创建一个单例,以维护与数据库的单个连接,但是它需要一个参数(Context)来实现。 - Aman Deep Gautam
7
“如果您尝试向构造函数提供参数,那么单例模式还有什么意义呢?” - 可以这样说:“如果您将整个应用程序设计为单个实例,那么命令行参数的作用是什么?”,答案是它非常有意义。现在可以说,这与单例类相当不同,除非该类实际上是从主方法接收args[]的Main类 - 那么它甚至是相同的东西。最终的论点可能是,这是一个非常特殊的情况。 - Dreamspace President
1
如果一个单例类在使用之前需要被初始化,那么客户端(即将使用它的类)将无法知道该单例是否已经被初始化。而且客户端可能没有正确的属性来初始化该单例。 - seal
显示剩余14条评论

44
我认为你需要类似于一个工厂的东西来实例化和重复使用带有各种参数的对象。可以通过使用同步的HashMapConcurrentHashMap将一个参数(例如Integer)映射到您的可参数化单例类。
虽然你可能会遇到这样的情况,即不应该使用单例类而是使用普通的非单例类(例如需要10,000个不同参数化的单例)。
这里是一个这样的存储示例:
public final class UsefulObjFactory {

    private static Map<Integer, UsefulObj> store =
        new HashMap<Integer, UsefulObj>();

    public static final class UsefulObj {
        private UsefulObj(int parameter) {
            // init
        }
        public void someUsefulMethod() {
            // some useful operation
        }
    }

    public static UsefulObj get(int parameter) {
        synchronized (store) {
            UsefulObj result = store.get(parameter);
            if (result == null) {
                result = new UsefulObj(parameter);
                store.put(parameter, result);
            }
            return result;
        }
    }
}
为了更进一步,Java中的enum可以被视为参数化的单例(或用作单例),尽管只允许固定数量的静态变量。
但是,如果您需要分布式解决方案,请考虑一些横向缓存解决方案。例如:EHCache、Terracotta等。
在这里,“分布式”指跨多个VM和可能跨多台计算机的意义上。

1
不是 单例;现在你有一个以上的它们。LOL - oxbow_lakes
我希望没有人介意我编辑代码中的名称;我可以想象这对新手来说真的很困惑。如果您不同意,请回滚。 - oxbow_lakes
1
是的,我們可以稱它們為Multitron,並在我看來仍然實現OP最初想要的目標。 - akarnokd
请不要建议任何人使用Terracotta,除非他们真的知道自己在做什么! - oxbow_lakes
@oxbow_lakes:也许现在是时候请求编辑器集成重构支持了 :). 至于Terracotta - 只是想根据OP在你的答案中的评论列出一些选项。 - akarnokd
显示剩余5条评论

41

你可以添加一个可配置的初始化方法,以便将实例化与获取分开。

public class Singleton {
    private static Singleton singleton = null;
    private final int x;

    private Singleton(int x) {
        this.x = x;
    }

    public static Singleton getInstance() {
        if(singleton == null) {
            throw new AssertionError("You have to call init first");
        }

        return singleton;
    }

    public synchronized static Singleton init(int x) {
        if (singleton != null)
        {
            // in my opinion this is optional, but for the purists it ensures
            // that you only ever get the same instance when you call getInstance
            throw new AssertionError("You already initialized me");
        }

        singleton = new Singleton(x);
        return singleton;
    }

}

您可以调用Singleton.init(123)一次进行配置,例如在您的应用程序启动时。


15

如果您想表明某些参数是必需的,也可以使用生成器模式。

    public enum EnumSingleton {

    INSTANCE;

    private String name; // Mandatory
    private Double age = null; // Not Mandatory

    private void build(SingletonBuilder builder) {
        this.name = builder.name;
        this.age = builder.age;
    }

    // Static getter
    public static EnumSingleton getSingleton() {
        return INSTANCE;
    }

    public void print() {
        System.out.println("Name "+name + ", age: "+age);
    }


    public static class SingletonBuilder {

        private final String name; // Mandatory
        private Double age = null; // Not Mandatory

        private SingletonBuilder(){
          name = null;
        }

        SingletonBuilder(String name) {
            this.name = name;
        }

        public SingletonBuilder age(double age) {
            this.age = age;
            return this;
        }

        public void build(){
            EnumSingleton.INSTANCE.build(this);
        }

    }


}

然后您可以按照以下方式来创建/实例化/参数化它:

public static void main(String[] args) {
    new EnumSingleton.SingletonBuilder("nico").age(41).build();
    EnumSingleton.getSingleton().print();
}

这很优雅。 - z atef

11

很惊讶没有人提到如何创建/检索日志记录器。例如,下面展示了如何检索Log4J 日志记录器

// Retrieve a logger named according to the value of the name parameter. If the named logger already exists, then the existing instance will be returned. Otherwise, a new instance is created.
public static Logger getLogger(String name)

有一些间接的层次,但关键部分在下面这个方法,这个方法几乎涵盖了它如何工作的全部内容。它使用哈希表来存储已存在的记录器,而键则是从名称派生的。如果给定名称的记录器不存在,则使用工厂创建记录器,然后将其添加到哈希表中。

69   Hashtable ht;
...
258  public
259  Logger getLogger(String name, LoggerFactory factory) {
260    //System.out.println("getInstance("+name+") called.");
261    CategoryKey key = new CategoryKey(name);
262    // Synchronize to prevent write conflicts. Read conflicts (in
263    // getChainedLevel method) are possible only if variable
264    // assignments are non-atomic.
265    Logger logger;
266
267    synchronized(ht) {
268      Object o = ht.get(key);
269      if(o == null) {
270        logger = factory.makeNewLoggerInstance(name);
271        logger.setHierarchy(this);
272        ht.put(key, logger);
273        updateParents(logger);
274        return logger;
275      } else if(o instanceof Logger) {
276        return (Logger) o;
277      } 
...

6
“带参数的单例不是单例”这个说法“并不完全正确”。我们需要从应用程序的角度进行分析,而不是从代码角度。
我们构建单例类是为了在一个应用程序运行中创建对象的单一实例。通过具有参数的构造函数,您可以在每次运行应用程序时为单例对象更改某些属性,从而为您的代码增加灵活性。这并不违反Singleton模式。如果你从代码的角度看待它,它看起来像是一种违规。
设计模式存在的目的是帮助我们编写灵活且可扩展的代码,而不是阻碍我们编写良好的代码。

15
这不是对问题的回答,应该是一条评论。 - Thierry J.

4

修改使用Bill Pugh的延迟初始化占位符惯用法的单例模式。这个方法是线程安全的,而且没有专门语言结构的开销(即volatile或synchronized):

public final class RInterfaceHL {

    /**
     * Private constructor prevents instantiation from other classes.
     */
    private RInterfaceHL() { }

    /**
     * R REPL (read-evaluate-parse loop) handler.
     */
    private static RMainLoopCallbacks rloopHandler = null;

    /**
     * SingletonHolder is loaded, and the static initializer executed, 
     * on the first execution of Singleton.getInstance() or the first 
     * access to SingletonHolder.INSTANCE, not before.
     */
    private static final class SingletonHolder {

        /**
         * Singleton instance, with static initializer.
         */
        private static final RInterfaceHL INSTANCE = initRInterfaceHL();

        /**
         * Initialize RInterfaceHL singleton instance using rLoopHandler from
         * outer class.
         * 
         * @return RInterfaceHL instance
         */
        private static RInterfaceHL initRInterfaceHL() {
            try {
                return new RInterfaceHL(rloopHandler);
            } catch (REngineException e) {
                // a static initializer cannot throw exceptions
                // but it can throw an ExceptionInInitializerError
                throw new ExceptionInInitializerError(e);
            }
        }

        /**
         * Prevent instantiation.
         */
        private SingletonHolder() {
        }

        /**
         * Get singleton RInterfaceHL.
         * 
         * @return RInterfaceHL singleton.
         */
        public static RInterfaceHL getInstance() {
            return SingletonHolder.INSTANCE;
        }

    }

    /**
     * Return the singleton instance of RInterfaceHL. Only the first call to
     * this will establish the rloopHandler.
     * 
     * @param rloopHandler
     *            R REPL handler supplied by client.
     * @return RInterfaceHL singleton instance
     * @throws REngineException
     *             if REngine cannot be created
     */
    public static RInterfaceHL getInstance(RMainLoopCallbacks rloopHandler)
            throws REngineException {
        RInterfaceHL.rloopHandler = rloopHandler;

        RInterfaceHL instance = null;

        try {
            instance = SingletonHolder.getInstance();
        } catch (ExceptionInInitializerError e) {

            // rethrow exception that occurred in the initializer
            // so our caller can deal with it
            Throwable exceptionInInit = e.getCause();
            throw new REngineException(null, exceptionInInit.getMessage());
        }

        return instance;
    }

    /**
     * org.rosuda.REngine.REngine high level R interface.
     */
    private REngine rosudaEngine = null;

    /**
     * Construct new RInterfaceHL. Only ever gets called once by
     * {@link SingletonHolder.initRInterfaceHL}.
     * 
     * @param rloopHandler
     *            R REPL handler supplied by client.
     * @throws REngineException
     *             if R cannot be loaded.
     */
    private RInterfaceHL(RMainLoopCallbacks rloopHandler)
            throws REngineException {

        // tell Rengine code not to die if it can't
        // load the JRI native DLLs. This allows
        // us to catch the UnsatisfiedLinkError
        // ourselves
        System.setProperty("jri.ignore.ule", "yes");

        rosudaEngine = new JRIEngine(new String[] { "--no-save" }, rloopHandler);
    }
}

我认为在getInstance中使用finally { RInterfaceHL.rloopHandler = null; }是一个好主意,因为如果我们不小心处理静态引用,它可能会导致内存泄漏。在你的情况下,这似乎不是问题,但我可以想象一种场景,传入的对象很大,只被RInterfaceHL构造函数用于获取某些值,而不是保留对它的引用。 - TWiStErRob
想法:在 getInstance 中,return SingletonHolder.INSTANCE 同样可以正常工作。我认为这里没有必要进行封装,因为外部类已经知道内部类的细节,它们是紧密耦合的:它知道在调用之前需要初始化 rloopHandler。此外,私有构造函数没有任何效果,因为内部类的私有内容对外部类是可用的。 - TWiStErRob
1
链接已经失效了。你是不是指的是 https://en.wikipedia.org/wiki/Initialization-on-demand_holder_idiom ? - Jorge Lavín

4

使用getter和setter方法设置变量,并将默认构造函数设置为私有。然后使用:

Singleton.getInstance().setX(value);

1
不明白为什么会被踩..这是个合法的答案。 :/ - Zack
14
因为这是一个垃圾答案。例如,想象一个系统,其中初始管理员的用户名和密码是构造函数参数。现在,如果我把它变成单例并按照你所说的做,我会得到用于获取和设置管理员的getter和setter,这并不完全是你想要的。因此,尽管你的选项在某些情况下可能有效,但它并没有真正回答问题所涉及的一般情况。(是的,我正在处理我描述的那个系统,如果不是因为分配任务中写了“在这里使用单例模式”,我就不会使用单例模式) - Jasper

4

如果你想创建一个作为上下文的单例类,并且希望从配置文件中读取参数,那么一个好的方法是在instance()方法内部读取文件中的参数。

如果Singleton类需要动态获取参数,则可以使用静态HashMap来存储不同实例,以确保每个参数只创建一个实例。


3
在您的示例中,您没有使用单例模式。请注意,如果您执行以下操作(假设 Singleton.getInstance 实际上是静态的):
Singleton obj1 = Singleton.getInstance(3);
Singleton obj2 = Singleton.getInstance(4);

然后,obj2.x的值为3,而不是4。如果需要这样做,请将其设置为普通类。如果值的数量很少且固定,则可以考虑使用枚举。如果您遇到过多的对象生成问题(通常不是这种情况),则可以考虑缓存值(并检查源或获取帮助,因为很明显如何构建缓存而不会造成内存泄漏的危险)。
您可能还想阅读本文,因为单例模式很容易被过度使用。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接