How to make the most of Java enums

Anytime you have a set of known constant values, an enum is a type-safe representation that prevents common problems.

April 9, 2021

Download a PDF of this article

[Java Magazine is pleased to republish this article from Michael Kölling, published in 2016, about enums. —Ed.]

Enumerations—or “enums” for short—are a restricted Java data type that lets a developer define a variable to be a set of predefined constants. The variable can be a member of only that set of constants. Enums are not used as much as they should be. They aren’t one of those big, bold, buzzy concepts that get people excited or force themselves on you. Rather they quietly improve code, making it more reliable and more readable.

There are several reasons why some developers don’t use enums. For one, early versions of Java did not have enums. Some programmers may have learned the platform before Java 5, when enums were added to the language, and they never got around to changing their habits. Others may have come from different languages that did not support enums. And lastly, some developers might not have felt the need to use enums because they were perfectly able to solve problems without them. None of these is a good reason to continue ignoring them.

Enums enable you to make your code significantly better: more robust, more type-safe, less error-prone, and more elegant. And these things matter.

When and why to use enums

Let’s examine the use of enums with an example. Suppose you want to write a text-based adventure game—something similar to Colossal Cave Adventure or Zork, two classic computer games. Those games have a set of command words that the user can type in. And let’s say the valid command words are go, look, take, help, and quit.

Somewhere in your code, you are likely to have a definition of those command words. In a straightforward first implementation, they might be defined in an array of strings, like the following:


private static final String[] validCommands = { 
   "go", "look", "take", "help", "quit"
};

Somewhere else in your program, you will have some code that reacts to these words being entered. The code then calls the right method to act on them. In the following code snippet, I assume that the String variable commandWord holds the word that was typed in:


switch (commandWord) {
   case "go":
      goRoom(secondWord);
      break;
   case "look":
      look();
      break;
   case "take":
      takeItem(secondWord);
      break;
   case "Help":
      printHelp();
      break;
   case "quit":
       quit();
       break;
}

(An alternative would be to define a sequence of int constants for these commands, and then translate the input string to a number and switch on the int constant. This popular variant has the same problems that I discuss with the solution explored here.)

What’s wrong with this solution? There are really two separate fundamental problems that immediately stand out: type safety and internationalization.

Let’s deal with type safety first. The short code segment presented above is quite straightforward and easy to understand. It works; right? In fact, it doesn’t. There is a bug in the code. Did you spot it?

The problem is that the help command has been mistyped in the switch statement as Help. Even though it was my intention that the commandWord should only ever be one of the strings listed as valid commands, there is nothing stopping you from assigning invalid commands or comparing it to invalid commands. Because the declared type is String, any string will do. In effect, the type system is not good enough. The declared type does not properly describe the set of acceptable values, and (logically) illegal values can be used without the type system being able to detect this.

Enums to the rescue

To avoid this problem, rewrite the code using enums. First write an enum declaration, as follows:


public enum CommandWord
{

   GO, LOOK, TAKE, HELP, QUIT
}

This declaration should be treated like a class and written in its own file. It defines the type CommandWord and the five listed names as valid values for that type. In other classes, you can then declare variables of this type and assign values, for example


CommandWord command = CommandWord.GO;

And, importantly, you can rewrite the switch statement to the following:


switch (commandWord) {
   case GO:
      goRoom(secondWord);
      break;
   case LOOK:
      look(); 
       break;
   case TAKE:
      takeItem(secondWord); 
       break;
   case HELP:
      printHelp(); 
       break;
   case QUIT:
quit(); break;
}

The definition of the command words in this version (as an enum, instead of a string array) is not only clearer and simpler. Now, it also creates type safety: If you mistype a case label or a value in an assignment, the compiler will detect this and notify you. This is a real win—you have the strong type system back that Java was designed for.

By the way, you can also use the double equals symbol (==) for checking equality, instead of the .equals() method used with strings.


if (command == CommandWord.QUIT) ...

What really is an enum?

Some people use enums only as described here and think of them as similar to int constants: named values that can be assigned and recognized later. But this is not the complete truth, and if you stop here, you have only scratched the surface and are missing out on some of the best features.

Enum declarations are full classes, and the values listed are constant names referring to separate instances of these classes. The enum declaration can contain fields, constructors, and methods, just like other classes. Here is an extended version of the previous enums.


public enum CommandWord {

   GO("go"), LOOK("look"), TAKE("take"),
   HELP("help"), QUIT("quit");
   private String commandString;

   CommandWord(String commandString)
   {
   this.commandString = commandString;
   }

   public String toString ()
   {
return commandString;
   }
}

The important aspects are the following:

  • Enum declarations are classes, and enum values refer to objects.
  • For every declared enum value, an instance of the class is created and assigned to that value.
  • No other instances of this class can be created later.
  • Every different enum value will refer to a different object, and the same value will always refer to the same object; this cannot be changed.
  • Enums create their own namespace, so different enum classes may use the same value, but these are kept separate. If, for example, I have an enum class BoardGames, the enum values BoardGames.GO and CommandWords.GO are separate and do not interfere with each other.

The last aspect—that no other instances may be created—is ensured by making the constructor private. It is not necessary to declare this explicitly: The constructor is automatically private, and it is an error to try to make it public.

The previous code will generate five enum objects—one for each value. And any reference in other code to CommandWord objects can be to only one or more of these enums. Any attempt to create other objects will generate a compile-time error.

Enums may contain any number of fields, constructors, and methods. The fundamental difference when compared with other classes is in how enum instances come into existence. While other classes start without any instances and provide a constructor for clients to create as many objects as they like, enums provide no constructor (to the outside) and instead provide a set of ready-made instances.

The fact that enum values are objects, not ints, is important. It means that enums provide not only identity but also state and behavior.

The full truth

The first question that now comes to mind may be this: If the constructor cannot be called from the outside, what is it used for? The answer lies in the modified syntax previously used for enumerating enum values. Instead of just


GO

as in the first version, you have now written


GO("go")

This extension—effectively adding a parameter list to the enum value—invokes the enum’s constructor. The expression within the parentheses is the actual parameter passed to the constructor. The enum object is still of class CommandWord, as before, but you are now storing a string attribute inside it. And the value for this attribute is passed in to the enum object via its constructor. You can store any number and type of attributes inside an enum object—that is, in instance fields—just as in any other object.

Read the CommandWord definition again—it should all slowly come together and start to make sense now.

The great advantage of this scheme is that you can now use a String again to recognize the typed word (for example, help) by comparing the input string against the command strings stored inside the enums, but the program logic remains independent of these strings. This is important.

Globalization

Earlier, I mentioned that another problem with the first version was internationalization: If you decide to translate the program into a different language (let’s say the help command is now hilfe), this runs into the danger of introducing errors. If you just change the command words in the array, the program will compile but not function. Why? None of the commands will be recognized. The problem is that the strings are not only used for input but also for the program logic. That is bad.

In the enum version, that problem has been resolved because the actual command strings are mentioned only once. If they change, they need to be changed only in one location, and the program logic works with logical values—the enum constants—that will continue to work. (In practice, the input commands would be read out of a locale-dependent text file, but the principle is the same.)

Under the hood

Enums are implemented as classes, and enum values are their instances. There is little special about this and knowing this helps you understand how they work and what can be done with them.

Enum classes all automatically inherit the Java standard class Enum from which they inherit some potentially useful methods (it also means that they cannot extend another class).

The inherited methods you should know about are name(), ordinal(), and the static method values().

The name() method returns the name exactly as defined in the enum value. The ordinal() method returns a numeric value that reflects the order in which the enums were declared, starting with zero. For example,


CommandWord cmd = CommandWord.GO;
System.out.println(cmd.name());
System.out.println(cmd.ordinal());

will print the following:


GO
0

In practice, these two methods are much less useful than you might first think. Your code typically should not depend on the actual enum name (so the name() method is not often useful; it is much better to override it and use the toString method for that purpose), and if you write your code well, you will rarely need the ordinal number.

The static values() method is more often useful. It returns an array of all enum values and can be used to iterate over them. Here’s an example.


CommandWord[] ca = CommandWord.values();

for (CommandWord cw : ca) {
   System.out.println(cw);
}

You can also use streams:


Arrays.stream(ca).forEach(System.out::println);

The enum singleton pattern

Once you understand how enums are really implemented under the hood (most importantly, that they are just classes with a different instance creation mechanism), you might discover some helpful ways to use them. One example I use regularly is to employ an enum to implement a singleton pattern.

A singleton is employed to ensure that only a single instance exists of a given class. It is often written by creating the instance in the class, making the constructor private, and providing a static factory method to hand out the instance, as in the following example:


public class Singleton {
   private static Singleton instance = new Singleton();

   public static Singleton getInstance()
   {
      return instance;
   }

   private Singleton()
   {
   ...
   }
}

The singleton instance can be accessed from the outside by writing


Singleton s = Singleton.getInstance();

A nice alternative is to use an enum to define the singleton.


public enum EasySingleton { INSTANCE;
}

No more work is needed, and the instance can easily be accessed from client code.


EasySingleton s = EasySingleton.INSTANCE;

Fields and methods can still be added to the singleton class as before. Enum instance creation is, by default, thread-safe, so this method is safe to use in a multithreaded application.

Conclusion

I hope this short introduction has demonstrated the advantages of enums. Anytime you find yourself defining a set of constant values, you should think of enums as the preferred way of representing them. This choice gives you type safety, support for internationalization, and warnings at compile time about possible coding errors. Overall, enums will make your code more readable and less prone to errors.

Dig deeper

Michael Kölling

Michael Kölling is a Java Champion and a professor at the University of Kent, England. He has published two Java textbooks and numerous papers on object orientation and computing education topics, and he is the lead developer of BlueJ and Greenfoot, two educational programming environments. Kölling is also a Distinguished Educator of the ACM.

Share this Page