Java 中带参数的单例模式

我在维基百科上读到一篇关于 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;
}
}

谢谢!


编辑: 我想我已经开始与我的愿望使用单例风暴的争议。让我解释一下我的动机,希望有人能提出一个更好的主意。我使用网格计算框架并行执行任务。一般来说,我有这样的东西:

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

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

151167 次浏览

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

一个带有参数的单例模式没有任何意义——如果你写:

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

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

使用 getters 和 setter 来设置变量,并将缺省构造函数设置为私有,然后使用:

Singleton.getInstance().setX(value);

你无法理解如何完成你正在尝试做的事情的原因可能是你正在尝试做的事情没有真正的意义。您希望使用不同的参数调用 getInstance(x),但总是返回相同的对象吗?当你调用 getInstance(2)getInstance(5)时,你想要什么样的行为?

如果你想要同一个对象,但是它的内部值不同,这是它仍然是一个单例对象的唯一方式,那么你根本不需要关心构造函数; 你只需要在对象出去的时候在 getInstance()中设置值。当然,您明白其他所有对单例的引用现在都有不同的内部值。

另一方面,如果您希望 getInstance(2)getInstance(5)返回不同的对象,那么您使用的不是单例模式,而是 Factory 模式。

我认为您需要类似于 工厂的东西来实例化和重用具有各种参数的对象。它可以通过使用同步的 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;
}
}
}

为了更进一步,Javaenums 也可以被认为(或用作)参数化单例,尽管只允许固定数量的静态变体。

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

1 在可能是多台计算机上跨越多个 VM 的意义上。

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

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

然后对象2.x 的值是3,而不是4。如果您需要这样做,那么将它设置为一个普通类。如果值的数量很小且固定,则可以考虑使用 enum。如果您遇到过多对象生成的问题(通常不是这种情况) ,那么您可以考虑缓存值(并检查源代码或获得相关帮助,因为如何构建缓存而不存在内存泄漏的危险是显而易见的)。

您也可能希望使用 读这篇文章,因为单例模式很容易被过度使用。

当然,Singleton 是一个“反模式”(假设静态变量状态的定义)。

如果您想要一组固定的不可变值对象,那么枚举就是方法。对于大型的、可能是开放式的值集,可以使用某种形式的 Repository-通常基于 Map实现。当然,在处理静态时,要小心使用线程(要么进行足够广泛的同步,要么使用 ConcurrentMap,要么检查另一个线程是否胜过您,要么使用某种形式的期货)。

我会把我的观点说得很清楚: 带参数的单例不是单例

根据定义,单例对象是您希望被实例化不超过一次的对象。如果您试图将参数提供给构造函数,那么单例模式的意义是什么?

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

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

在任何情况下,实例化将始终是无参数的。否则,您的单例不是单例。

改良使用 Bill Pugh 对需求持有者习惯用法的初始化的单例模式。这是线程安全的,没有专门的语言构造的开销(比如易失性或者同步性) :

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

Singleton 是反模式的另一个原因是,如果根据推荐编写,使用私有构造函数,它们很难子类化并配置为在某些单元测试中使用。例如,在维护遗留代码时将需要。

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

这并不完全是一个单例模式,但可能会解决您的问题。

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

如果我们把这个问题看作“如何使用状态实现单例”,那么就没有必要将状态作为构造函数参数传递。我同意在获得单例实例后初始化状态或使用 set 方法的文章。

另一个问题是: 使用带状态的单例模式好吗?

令人惊讶的是没有人提到如何创建/检索日志记录器。

// 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      }
...

我们就不能这么做吗:

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

带参数的单例不是单例”语句是 不完全正确。我们需要从应用程序的角度而不是从代码的角度来分析这个问题。

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

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

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

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)一次来配置它,例如在应用程序启动时。

不管有些人可能断言什么,这里是一个构造函数中包含参数的单例

public class Singleton {


private static String aParameterStored;


private static final Singleton instance = new Singleton("Param to set");


private Singleton() {
// do nothing
}


private Singleton(String param) {
aParameterStored = param;
}


public static Singleton getInstance() {
return instance;
}


/*
* ... stuff you would like the singleton do
*/
}

单例模式表示:

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

在这个例子中是受到尊重的。

为什么不直接设置属性?这是一个典型的例子,展示了我们如何得到一个带参数的单例构造函数,但是在某些情况下可能会很有用。例如在继承情况下,强制单例设置一些超类属性。

如果您想创建一个作为 Context 的 Singleton 类,一个好的方法是拥有一个配置文件并从 instance ()中的文件中读取参数。

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

我认为这是一个普遍的问题。将单例模式的“初始化”与单例模式的“获取”分离可能有效(本例使用了双重检查锁定的变体)。

public class MySingleton {


private static volatile MySingleton INSTANCE;


@SuppressWarnings("UnusedAssignment")
public static void initialize(
final SomeDependency someDependency) {


MySingleton result = INSTANCE;


if (result != null) {
throw new IllegalStateException("The singleton has already "
+ "been initialized.");
}


synchronized (MySingleton.class) {
result = INSTANCE;


if (result == null) {
INSTANCE = result = new MySingleton(someDependency);
}
}
}


public static MySingleton get() {
MySingleton  result = INSTANCE;


if (result == null) {
throw new IllegalStateException("The singleton has not been "
+ "initialized. You must call initialize(...) before "
+ "calling get()");
}


return result;
}


...
}

我不敢把这个作为一个答案,但我不明白为什么没有人想到这一点,也许这个答案也已经给出了,我只是不明白。

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()每次返回相同的实例,我认为这可以工作。 如果这是错的太多,我会删除它,我只是对这个话题感兴趣。

需要补充的是,如果需要参数只初始化一次,而且不能被覆盖,那么只需要实现一个检查,如果有人试图再次初始化它们,就会抛出异常。例如:

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