在Java中使用带参数的单例

时间:2009-06-26 20:02:45

标签: 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;
    }
}

虽然我非常喜欢这个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;
    }
}

谢谢!


编辑:我想我已经开始引发争议,我希望使用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).
    }
}

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

21 个答案:

答案 0 :(得分:155)

我会明确指出:带参数的单身人士不是单身人士

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

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

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

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

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

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

答案 1 :(得分:38)

我认为你需要类似工厂之类的东西来让实例化和重用各种参数的对象。可以通过使用同步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也可以被认为(或用作参数化单例),尽管只允许固定数量的静态变体。

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

1 意味着可能在多台计算机上跨越多个虚拟机。

答案 2 :(得分:10)

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

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)一次进行配置。

答案 3 :(得分:10)

如果要显示某些参数是必需的,也可以使用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);
        }

    }


}

然后您可以创建/实例化/参数化,如下所示:

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

答案 4 :(得分:7)

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

Singleton.getInstance().setX(value);

答案 5 :(得分:5)

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

// 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)

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

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 :(得分:4)

&#34; 带参数的单身人士不是单身人士&#34;声明不完全正确。我们需要从应用程序的角度而不是从代码的角度来分析它。

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

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

答案 7 :(得分:4)

修改使用Bill Pugh's initialization on demand holder idiom的单例模式。这是线程安全的,没有专门的语言结构(即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);
    }
}

答案 8 :(得分:3)

单身人士反模式的另一个原因是,如果根据建议编写,使用私有构造函数,他们很难在某些单元测试中进行子类化和配置。例如,维护遗留代码是必需的。

答案 9 :(得分:3)

在您的示例中,您没有使用单身人士。请注意,如果您执行以下操作(假设Singleton.getInstance实际上是静态的):

Singleton obj1 = Singleton.getInstance(3);
Singleton obj2 = Singleton.getInstance(4);

然后obj2.x的值为3,而不是4.如果需要这样做,请将其设为普通类。如果值的数量很小并且已修复,则可以考虑使用enum。如果您遇到过多的对象生成问题(通常不是这种情况),那么您可以考虑缓存值(并检查源代码或获取帮助,因为很明显如何构建缓存而没有内存泄漏的危险)。

你也可能想要read this article,因为单身人士很容易被过度使用。

答案 10 :(得分:3)

你无法理解如何完成你想要做的事情的原因可能是你想要做的事情并不真正有意义。你想用不同的参数调用getInstance(x),但总是返回相同的对象?当您致电getInstance(2)然后getInstance(5)时,您想要什么行为?

如果你想要相同的对象,但是它的内部值是不同的,这是它仍然是单身的唯一方法,那么你根本不需要关心构造函数;你只需在getInstance()上设置对象出路的值。当然,您了解所有对单身人士的其他引用现在都有不同的内部价值。

如果您希望getInstance(2)getInstance(5)返回不同的对象,另一方面,您没有使用Singleton模式,那么您正在使用工厂模式。

答案 11 :(得分:2)

如果要创建用作Context的Singleton类,一个好方法是获取配置文件并从instance()中的文件中读取参数。

如果在程序运行期间动态获取了提供Singleton类的参数,只需使用在Singleton类中存储不同实例的静态HashMap,以确保每个参数只创建一个实例。

答案 12 :(得分:1)

如果我们将问题视为“如何使单态与状态”,那么就没有必要将状态作为构造函数参数传递。我同意在获取单例实例后初始化状态或使用set方法的帖子。

另一个问题是:单身人士与州有关系吗?

答案 13 :(得分:1)

我们不能做这样的事情:

public class Singleton {

    private int x;

    // 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(int x) {
        Singleton instance = SingletonHolder.INSTANCE;
        instance.x = x;
        return instance;
    }
}

答案 14 :(得分:1)

尽管有些人可能断言,但这里是一个在构造函数中具有参数的单例

public function prePersist(sharedBehaviourInterface $dbFile, LifecycleEventArgs $event)
{
    $entity = $event->getObject();

    if ($entity instanceof ClassA) {
        // Do something
    } elseif ($entity instanceof ClassB) {
        // Something else
    } else {
        // Nah, none of the above...
        return;
    }
}

单身模式说:

  • 确保只存在单个类的一个实例
  • 提供对该实例的全局访问权。

这个例子受到尊重。

为什么不直接设置属性?它的教科书案例展示了我们如何获得具有带参数的构造函数的单例,但在某些情况下它可能很有用。例如,在继承的情况下,强制单例设置一些超类属性。

答案 15 :(得分:0)

要添加的东西,如果希望参数应该只初始化一次并且不能被覆盖,那么如果有人试图再次初始化它们,只需实现检查并抛出异常。例如:

public class Service {

private String host = null;
private String port = null;

private Service() {
}

private static class ServiceSingletonHolder {

    private static final Service INSTANCE = new Service();
}

public static Service getInstance() {
    return ServiceSingletonHolder.INSTANCE;
}

public void initilize(String host, String port) {
    if (this.host != null && host != null) {
        throw new IllegalArgumentException("host can not be overwritten");
    }

    if (this.port != null && port != null) {
        throw new IllegalArgumentException("port can not be overwritten");
    }

    this.host = host;
    this.port = port;
}
}

答案 16 :(得分:0)

我不敢将其发布为答案,但是我不明白为什么没人考虑这个,也许这个答案也已经给出了,我只是不理解。

public class example  {
    private volatile static example instance;

    private String string;
    private int iInt = -1; //any number you know you don't want to use here

  private example() {

    //In case someone uses the private method to create a new Instance
    if (instance != null){
      throw new RuntimeException("Use getInstance() method to get the single instance of this class.");
    }
  }

  public synchronized static example getIsntance(){
    if(instance == null){
      instance = new example();
    }
    return instance;
  }

public void methodDoingWork(){
    if(checkInit()){
      //DoSome
    }
  }

  private boolean checkInit(){
    boolean filled = (this.string != null) && (this.iInt != -1);
    return filled;
  }

  public void setString(String string) {
    if(this.string == null){
      this.string = string;
    }else{
      throw new RuntimeException("You try to override an already setValue"); 
    }
  }

  public void setiInt(int iInt) {
    if(this.iInt == -1){
      this.iInt = iInt;
    }else{
      throw new RuntimeException("You try to override an already setValue");
    }
  }
}

由于getInstance()每次都返回相同的实例,因此我认为这可能有效。 如果这在很大程度上是错误的,我将其删除,我只是对该主题感兴趣。

答案 17 :(得分:0)

这不是一个单身人士,但可能会解决你的问题。

public class KamilManager {

  private static KamilManager sharedInstance;

  /**
   * This method cannot be called before calling KamilManager constructor or else
   * it will bomb out.
   * @return
   */
  public static KamilManager getInstanceAfterInitialized() {
    if(sharedInstance == null)
        throw new RuntimeException("You must instantiate KamilManager once, before calling this method");

    return sharedInstance;
}

  public KamilManager(Context context, KamilConfig KamilConfig) {
    //Set whatever you need to set here then call:
  s  haredInstance = this;
  }
}

答案 18 :(得分:-1)

我认为这是一个常见问题。将单例的“初始化”与单例的“get”分开可能有效(此示例使用双重检查锁定的变体)。

ViewModel

答案 19 :(得分:-1)

Singleton当然是一种“反模式”(假设一个带有可变状态的静态定义)。

如果你想要一组固定的不可变值对象,那么枚举就是你的选择。对于大的,可能是开放式的值集,您可以使用某种形式的存储库 - 通常基于Map实现。当然,当你处理静态时要小心线程化(要么足够广泛地同步,要么使用ConcurrentMap检查另一个线程是否没有击败你或使用某种形式的期货)。

答案 20 :(得分:-4)

单身人士通常被视为anti-patterns,不应使用。它们不会使代码易于测试。

带有参数的单身人士无论如何都没有意义 - 如果你写下了会发生什么:

Singleton s = SingletonHolder.getInstance(1);
Singleton t = SingletonHolder.getInstance(2); //should probably throw IllegalStateException

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