在Java中使用带参数的单例

130 java oop singleton anti-patterns

我正在阅读维基百科上的Singleton文章,我遇到了这个例子:

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;
    }
}
Run Code Online (Sandbox Code Playgroud)

虽然我非常喜欢这个Singleton的行为方式,但我看不出如何调整它以将参数合并到构造函数中.在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;
    }
}
Run Code Online (Sandbox Code Playgroud)

谢谢!


编辑:我想我已经开始引发争议,因为我希望使用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).
    }
}
Run Code Online (Sandbox Code Playgroud)

会发生的是,即使我只是将对数据的引用传递给所有任务,但当任务被序列化时,数据会一遍又一遍地被复制.我想要做的是在所有任务中共享对象.当然,我可能会像这样修改类:

// 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).
    }
}
Run Code Online (Sandbox Code Playgroud)

正如您所看到的,即使在这里,我遇到的问题是传递不同的文件路径在第一个传递之后没有任何意义.这就是为什么我喜欢在答案中发布的商店的想法.无论如何,我想将这个逻辑抽象为Singleton类,而不是包含在run方法中加载文件的逻辑.我不会提供另一个例子,但我希望你能得到这个想法.请让我听听你的想法,以更优雅的方式来完成我想要做的事情.再次感谢你!

Yuv*_*dam 161

我会明确指出:带参数的单例不是单例.

根据定义,单例是一个您想要实例化的对象,不超过一次.如果您尝试将参数提供给构造函数,那么单例的重点是什么?

你有两个选择.如果您希望使用某些数据初始化单例,可以在实例化后使用数据加载它,如下所示:

SingletonObj singleton = SingletonObj.getInstance();
singleton.init(paramA, paramB); // init the object with data
Run Code Online (Sandbox Code Playgroud)

如果你的单例正在执行的操作是重复的,并且每次都有不同的参数,你也可以将参数传递给正在执行的main方法:

SingletonObj singleton = SingletonObj.getInstance();
singleton.doSomething(paramA, paramB); // pass parameters on execution
Run Code Online (Sandbox Code Playgroud)

在任何情况下,实例化将始终是无参数的.否则你的单身人士不是单身人士.

  • 对不起,那不是真的.在某些情况下,您必须传递动态创建的参数,这些参数对于孔应用程序运行时保持不变.所以你不能在单例中使用常量,但必须在创建它时传递该常量.在经过一次与洞时间相同的常数之后.如果你需要构造函数中的特定常量,setter不会完成这项工作. (119认同)
  • 如果您只需要一个类的一个实例用于应用程序的整个生命周期,但是您需要在启动时为该实例提供值,为什么这不再是单例? (45认同)
  • 与您的假设相反的一个例子是 android 中的数据库助手类。最好的做法是为这个类使用一个单例来维护一个到数据库的连接,但它需要一个参数(`Context`)。 (3认同)
  • "如果你试图将参数提供给构造函数,那么单例是什么意思?" - 也可以说:"如果你使整个应用程序成为单个实例,命令行参数的重点是什么?",答案是它很有意义.现在可以说这与单例类有很大不同,除非该类实际上是从main方法接收args []的Main类 - 那么它甚至是相同的东西.可能只是最后的论点是,这是一个相当特殊的情况. (3认同)

aka*_*okd 39

我认为你需要像工厂这样的东西来拥有实例化和重用各种参数的对象.它可以通过使用synchronized HashMapConcurrentHashMap将参数(Integer例如)映射到'singleton'可参数化类来实现.

虽然您可能会使用常规的非单例类(例如需要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;
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

为了进一步推动它,Java enum也可以被认为(或用作参数化单例),尽管只允许固定数量的静态变体.

但是,如果您需要分布式1解决方案,请考虑一些横向缓存解决方案.例如:EHCache,Terracotta等.

1可能是在多台计算机上跨越多个VM.


mig*_*uel 12

您可以添加初始化方法,以便将实例化与获取分开.

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;
    }

}
Run Code Online (Sandbox Code Playgroud)

然后,您可以Singleton.init(123)在应用启动中调用一次来配置它,例如.


ger*_*ico 11

如果要显示某些参数是必需的,也可以使用Builder模式.

    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);
        }

    }


}
Run Code Online (Sandbox Code Playgroud)

然后你可以创建/实例化/参数化它如下:

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


Alb*_*oPL 6

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

Singleton.getInstance().setX(value);
Run Code Online (Sandbox Code Playgroud)

  • 因为这是垃圾回答.例如,假设一个系统,其中初始管理员的初始用户名和密码是构造函数参数.现在,如果我把它作为一个单身人士并按照你的说法行事,那么我会得到管理员的getter和setter,这不是你想要的.因此,虽然您的选项在某些情况下可能有效,但它并不能真正回答问题的一般情况.(是的,我正在研究我描述的系统而不是,如果不是因为赋值说"在这里使用单例模式"这个事实,我不会使用单例模式) (11认同)

小智 6

带有参数的单例不是单例 ”语句并不完全正确。我们需要从应用程序的角度而不是从代码的角度进行分析。

我们构建单例类以在一个应用程序运行中创建对象的单个实例。通过使用带有参数的构造函数,您可以在代码中构建灵活性,以在每次运行应用程序时更改单例对象的某些属性。这不违反Singleton模式。如果从代码角度来看,这似乎是一种违规。

设计模式可以帮助我们编写灵活且可扩展的代码,而不会妨碍我们编写良好的代码。

  • 这不是OP问题的答案,应该是评论。 (10认同)

wan*_*ghq 5

很惊讶没有人提到如何创建/检索记录器.例如,下面显示了如何检索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)
Run Code Online (Sandbox Code Playgroud)

有一些层次的间接,但关键部分是下面的方法,它几乎告诉它如何工作的一切.它使用哈希表来存储现有的记录器,密钥是从名称派生的.如果给定名称不存在记录器,它将使用工厂创建记录器,然后将其添加到哈希表中.

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      } 
...
Run Code Online (Sandbox Code Playgroud)


oxb*_*kes -3

单例通常被认为是反模式,不应该使用。它们不会使代码易于测试。

无论如何,带有参数的单例是没有意义的 - 如果你写下会发生什么:

Singleton s = SingletonHolder.getInstance(1);
Singleton t = SingletonHolder.getInstance(2); //should probably throw IllegalStateException
Run Code Online (Sandbox Code Playgroud)

您的单例也不是线程安全的,因为多个线程可以同时调用,从而getInstance导致创建多个实例(可能具有不同的 值x)。

  • 是的,这是有争议的;因此我使用“一般”这个词。我认为可以公平地说,它们通常被认为是一个坏主意 (2认同)