14

Traditionally, a singleton is usually implemented as

public class Foo1
{
    private static final Foo1 INSTANCE = new Foo1();

    public static Foo1 getInstance(){ return INSTANCE; }

    private Foo1(){}

    public void doo(){ ... }
}

With Java's enum, we can implement a singleton as

public enum Foo2
{
    INSTANCE;

    public void doo(){ ... }
}

As awesome as the 2nd version is, are there any downsides to it?

(I gave it some thoughts and I'll answer my own question; hopefully you have better answers)

1
  • 16
    The downside is that it is a singleton. A totally overrated (cough) "pattern" Dec 14, 2012 at 0:58

3 Answers 3

32

Some problems with enum singletons:

Committing to an implementation strategy

Typically, "singleton" refers to an implementation strategy, not an API specification. It is very rare for Foo1.getInstance() to publicly declare that it'll always return the same instance. If needed, the implementation of Foo1.getInstance() can evolve, for example, to return one instance per thread.

With Foo2.INSTANCE we publicly declare that this instance is the instance, and there's no chance to change that. The implementation strategy of having a single instance is exposed and committed to.

This problem is not crippling. For example, Foo2.INSTANCE.doo() can rely on a thread local helper object, to effectively have a per-thread instance.

Extending Enum class

Foo2 extends a super class Enum<Foo2>. We usually want to avoid super classes; especially in this case, the super class forced on Foo2 has nothing to do with what Foo2 is supposed to be. That is a pollution to the type hierarchy of our application. If we really want a super class, usually it's an application class, but we can't, Foo2's super class is fixed.

Foo2 inherits some funny instance methods like name(), cardinal(), compareTo(Foo2), which are just confusing to Foo2's users. Foo2 can't have its own name() method even if that method is desirable in Foo2's interface.

Foo2 also contains some funny static methods

    public static Foo2[] values() { ... }
    public static Foo2 valueOf(String name) { ... }
    public static <T extends Enum<T>> T valueOf(Class<T> enumType, String name)

which appears to be nonsensical to users. A singleton usually shouldn't have pulbic static methods anyway (other than the getInstance())

Serializability

It is very common for singletons to be stateful. These singletons generally should not be be serializable. I can't think of any realistic example where it makes sense to transport a stateful singleton from one VM to another VM; a singleton means "unique within a VM", not "unique in the universe".

If serialization really does make sense for a stateful singleton, the singleton should explicitly and precisely specify what does it means to deserialize a singleton in another VM where a singleton of the same type may already exist.

Foo2 automatically commits to a simplistic serialization/deserialization strategy. That is just an accident waiting to happen. If we have a tree of data conceptually referencing a state variable of Foo2 in VM1 at t1, through serialization/deserialization the value becomes a different value - the value of the same variable of Foo2 in VM2 at t2, creating a hard to detect bug. This bug won't happen to the unserializable Foo1 silently.

Restrictions of coding

There are things that can be done in normal classes, but forbidden in enum classes. For example, accessing a static field in the constructor. The programmer has to be more careful since he's working in a special class.

Conclusion

By piggybacking on enum, we save 2 lines of code; but the price is too high, we have to carry all the baggages and restrictions of enums, we inadvertently inherit "features" of enum that have unintended consequences. The only alleged advantage - automatic serializability - turns out to be a disadvantage.

13
  • 2
    -1: Your discussion of serialization is wrong. The mechanism is not simplistic as enums are treated very differently from regular instances during deserialization. The problem described does not occur as the actual deserialization mechanism does not modify a "state variable".
    – scarfridge
    Dec 14, 2012 at 15:15
  • see this example of confusion: coderanch.com/t/498782/java/java/… Dec 14, 2012 at 15:24
  • 3
    Actually the linked discussion stresses my point. Let me explain what I understood as the problem you claim to exist. Some object A references a second object B. The singleton instance S references B as well. Now we deserialize a previously serialized instance of the enum-based singleton (which, at the time of serialisation, referenced B' != B). What actually happens is, that A and S reference B, because B' will not be serialized. I thought you wanted to express that A and S do not reference the same object anymore.
    – scarfridge
    Dec 14, 2012 at 16:08
  • 1
    Maybe we are not really talking about the same problem?
    – scarfridge
    Dec 14, 2012 at 16:10
  • 1
    @Kevin Krumwiede: Constructor<?> c=EnumType.class.getDeclaredConstructors()[0]; c.setAccessible(true); EnumType f=(EnumType)MethodHandles.lookup().unreflectConstructor(c).invokeExact("BAR", 1);, e.g. a really nice example would be: Constructor<?> c=Thread.State.class.getDeclaredConstructors()[0]; c.setAccessible(true); Thread.State f=(Thread.State)MethodHandles.lookup().unreflectConstructor(c).invokeExact("RUNNING_BACKWARD", -1); ;^), tested under Java 7 and Java 8…
    – Holger
    Jul 17, 2015 at 18:38
7

An enum instance is dependant on the class loader. ie if you have a second class loader that does not have the first class loader as a parent loading the same enum class you can get multiple instances in memory.


Code Sample

Create the following enum, and put its .class file into a jar by itself. ( of course the jar will have the correct package/folder structure )

package mad;
public enum Side {
  RIGHT, LEFT;
}

Now run this test, making sure that there is no copies of the above enum on the class path:

@Test
public void testEnums() throws Exception
{
    final ClassLoader root = MadTest.class.getClassLoader();

    final File jar = new File("path to jar"); // Edit path
    assertTrue(jar.exists());
    assertTrue(jar.isFile());

    final URL[] urls = new URL[] { jar.toURI().toURL() };
    final ClassLoader cl1 = new URLClassLoader(urls, root);
    final ClassLoader cl2 = new URLClassLoader(urls, root);

    final Class<?> sideClass1 = cl1.loadClass("mad.Side");
    final Class<?> sideClass2 = cl2.loadClass("mad.Side");

    assertNotSame(sideClass1, sideClass2);

    assertTrue(sideClass1.isEnum());
    assertTrue(sideClass2.isEnum());
    final Field f1 = sideClass1.getField("RIGHT");
    final Field f2 = sideClass2.getField("RIGHT");
    assertTrue(f1.isEnumConstant());
    assertTrue(f2.isEnumConstant());

    final Object right1 = f1.get(null);
    final Object right2 = f2.get(null);
    assertNotSame(right1, right2);
}

And we now have two objects representing the "same" enum value.

I agree that this is a rare and contrived corner case, and almost always an enum can be used for a java singleton. I do it myself. But the question asked about potential downsides and this note of caution is worth knowing about.

5
  • Can you find any references to that concern?
    – user40980
    Jul 10, 2013 at 0:46
  • I have now edited and enhanced my initial answer with example code. Hopefully they will help illustrate the point and answer MichaelT's query as well.
    – Mad G
    Jul 11, 2013 at 10:40
  • @MichaelT : I hope that answers your question :-)
    – Mad G
    Jul 15, 2013 at 17:52
  • So, if the reason for use of enum (instead of class) for singleton was only its safety, there is not any reason for it now... Excellent, +1
    – Gangnus
    Oct 2, 2015 at 21:06
  • 1
    Will the "traditional" singleton implementation behave as expected even with two different class loaders?
    – Ron Klein
    Sep 14, 2017 at 19:31
3

enum pattern can't be used for any Class that would throw an exception in the constructor. If this is required, use a factory:

class Elvis {
    private static Elvis self = null;
    private int age;

    private Elvis() throws Exception {
        ...
    }

    public synchronized Elvis getInstance() throws Exception {
        return self != null ? self : (self = new Elvis());
    }

    public int getAge() {
        return age;
    }        
}

Not the answer you're looking for? Browse other questions tagged or ask your own question.