2

From other answers on here, I see that essentially subclassing a singleton contradicts what it means to have a singleton. However, I don't quite understand why it violates this property when getInstance returns the same shared instance of the superclass. For example:

class Singleton {
    private static Singleton instance = null;
    protected Singleton() {

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

class SubclassSingleton extends Singleton {
    public SubclassSingleton() {

    }
}


SubclassSingleton x =  new SubclassSingleton();
SubclassSingleton y = new SubclassSingleton();
System.out.println(x.getInstance() == y.getInstance()); // true

Yes, technically x and y are instances of Singleton, but getting their instance still results in a shared resource, and the constructor (from my understanding) after constructing x has essentially no effect—rather, the static method getInstance() will return the shared construction of the parent class. So, I'm having trouble understanding how this violates essentially having one shared instance of Singleton, despite being subclassed.

rb612
  • 5,280
  • 3
  • 30
  • 68

2 Answers2

4

tl;dr: While I think it is possible to design a singleton in such a way that it can be subclassed, the class must be designed with special care. In most cases, I think it is not worth the effort.


I would not say that subclassing is an inherent violation of the singleton principle, but some extra care has to be taken when designing such a class. To understand why, let us take a look at the opposing definitions in this problem.

The first one is the definition is the Singleton Pattern. It essentially statest that, for this class, there will be at most one instance of that class, and all classes use that same instance. In Java, this typically means that the singleton cannot be re-initialized (i.e. a second call to new ... must be prevented). Why? Because somewhere in the program, some part could hold a reference x to the "old" singleton instance. When we re-initialize the singleton, x will still refer to the old singleton, while all future calls to the singleton's accessor will hold a reference to the new singleton. We have two instances of the same class, therefore we violate the singleton.

The definition opposing the singleton wrt. subclass is the Liskov Substitution Laws. It, in essence, states that every subclass of a class X must be usable in X's place, and the program should still work. This is why a Square and a Rectangle should never stand is an inheritance relation (one needs only one property to be defined [length], while the other needs two [width & height]).

This means that, for the scope of the singleton, all subclass-instances count as instances of the superclass as well. We have to control not only instantiation of the class in question, but also of all (!) its subclasses, even the ones we have not under control. Worst case scenario we do not even know all classes that subclass our singleton.

I coded up a little example that covers the cases I can see. Note that this example is not guaranteed to be perfect, it just shows to what extend we have to go to get our Singleton subclassable properly.

class Singleton {
  private static volatile Singleton INSTANCE = null;

  private static Supplier<? extends Singleton> factory;

  public static void setSingletonFactory(Supplier<? extends Singleton> factory) {
    Singleton.factory = factory;
  }

  public static Singleton getInstance() {
    if (INSTANCE == null) {
      synchronized (Singleton.class) {
        if (INSTANCE == null) {
          INSTANCE = factory.get();
        }
      }
    }
    return INSTANCE;
  }

  protected Singleton() {
    if (INSTANCE == null) {
      synchronized (Singleton.class) {
        if (INSTANCE == null) {
          // Set attribute of Singleton as necessary
        } else {
          throw cannotReinitializeSingletonIllegalStateException();
        }
      }
    } else {
      throw cannotReinitializeSingletonIllegalStateException();
    }
  }

  private static IllegalStateException cannotReinitializeSingletonIllegalStateException() {
    return new IllegalStateException("Cannot reinitialize Singleton");
  }

  public static void main(String... args) {
    Singleton.setSingletonFactory(SubSingleton.SUB_SINGLETON_FACTORY);
    Singleton instanceOne = Singleton.getInstance();
    Singleton instanceTwo = Singleton.getInstance();

    System.out.println(instanceOne == instanceTwo);

    try {
      SubSingleton.SUB_SINGLETON_FACTORY.get();
    } catch (IllegalStateException e) {
      System.out.println("Rightfully thrown " + e);
    }
  }
}

class SubSingleton extends Singleton {
  public static final Supplier<SubSingleton> SUB_SINGLETON_FACTORY = SubSingleton::new;

  private SubSingleton() {}
}

Ideone example

The double-checked locking at two instances (getInstance() and the constructor) is in deed necessary, if the call to getInstance() is performance critical, i.e. we cannot afford to lock each call to it. Furthermore, if the singleton is used as intended, a call to getInstance() should never throw. In the constructor, we need the double-checked locking to enforce the singleton property. In getInstance(), we need it to prevent IllegalStateExceptions from being thrown prematurely.

Seeing the above proof of concept, it is apparent that some care must be taken. There is still the possibility that somewhere in our program, an instance of a subclass is created before the first call to getInstance() is executed. This would mean that any call to getInstance() will throw an IllegalStateException (we have basically bricked the system). Setting INSTANCE from Singleton's constructor (INSTANCE = this) is not an option since this is - at this point in time - a premature object.

As far as I know, there is no possibility to both have the flexibility of injecting the factory and prevent constructors of a subcalss being called outside the intended way.

Turing85
  • 18,217
  • 7
  • 33
  • 58
3

Now the primary, overarching property of a singleton class is that you are prevented from make more than one instance of the class.

In your example, you are clearly making multiple instances of SubclassSingleton. Therefore, it is not a singleton.

SubclassSingleton x = new SubclassSingleton();
SubclassSingleton y = new SubclassSingleton();
System.out.println(x == y); // false

Your code that is comparing x.getInstance() and y.getInstance() is moot, because you are not even calling an instance method there. You have declared getInstance as a static method of Singleton ...

And even if you were calling instance methods, the fact that x.someMethod() == y.someMethod() does not prove that x and y are the same object ... or (in general) that they are equivalent.


So how does this violate the singelton-ness of Singleton?

Simple.

Each instance of SubclassSingleton is also a Singleton ... because SubclassSingleton extends Singleton. Therefore, when you do this:

SubclassSingleton x = new SubclassSingleton();
SubclassSingleton y = new SubclassSingleton();

you have created two distinct Singleton objects. That violates the singleton-ness property of the Singleton class. For example:

Singleton x = new SubclassSingleton();
Singleton y = new SubclassSingleton();
System.out.println(x == y); // false
Stephen C
  • 698,415
  • 94
  • 811
  • 1,216
  • Thanks Stephen. Yes I think you brought up some good points. I suppose I was trying to get at the fact that in essence, there’s only one instance of the Singleton base class, with emphasis on it being the base class, as derived types still do count as being instances of the base class. When you say that by creating two instances of the subclass, we’re creating two distinct Singleton objects, is that all instance members of Singleton can differ amongst SubclassSingleton objects? Say I add “name” as an instance variable to Singleton. “name” can be changed in x without “name” changing in y. – rb612 Jun 22 '19 at 20:05
  • The point is that *"there’s only one instance of the Singleton base class"* is not sufficient ... according to the normal definition of "singleton-ness". – Stephen C Jun 23 '19 at 00:40