How to use typesafe enums in Java

how-to
18 Jun 202411 mins
JavaProgramming LanguagesSoftware Development

Typesafe enums offer a better alternative to Java's traditional enumerated types. Here's how to use typesafe enums correctly in your Java code.

Lifebuoy and anchor on a white background.
Credit: Yarkovoy/Shutterstock

This article introduces you to the difference between enumerated types and typesafe enums. You will learn how to declare a typesafe enum and use it in a switch statement, and you’ll see how to customize a typesafe enum by adding data and behaviors. We’ll also take a look at java.lang.Enum<E extends Enum<E>>, which is the base class for all typesafe enums.

What you’ll learn in this Java tutorial

  • Why use typesafe enums and not enumerated types
  • How to use typesafe enums in switch statements
  • How to add data and behaviors to typesafe enums
  • Details and examples of the Enum class
download
Download the source code for examples in this tutorial. Created by Jeff Friesen.
 

Why use typesafe enums, not enumerated types

An enumerated type specifies a set of related constants as its values. Examples include the days in a week, the standard north/south/east/west compass directions, a currency’s coin denominations, and a lexical analyzer’s token types.

Enumerated types have traditionally been implemented as sequences of integer constants, which is demonstrated by the following set of direction constants:


static final int DIR_NORTH = 0;
static final int DIR_WEST = 1;
static final int DIR_EAST = 2;
static final int DIR_SOUTH = 3;

There are several problems with this approach:

  • It lacks type safety: Because an enumerated type constant is just an integer, any integer can be specified where the constant is required. Furthermore, addition, subtraction, and other math operations can be performed on these constants; for example, (DIR_NORTH + DIR_EAST) / DIR_SOUTH), which is meaningless.
  • The namespace isn’t present: An enumerated type’s constants must be prefixed with some kind of (hopefully) unique identifier (e.g., DIR_) to prevent collisions with another enumerated type’s constants.
  • The code is brittle: Because enumerated type constants are compiled into class files where their literal values are stored (in constant pools), changing a constant’s value requires that these class files and those application class files that depend on them be rebuilt. Otherwise, undefined behavior will occur at runtime.
  • Not enough information: When a constant is printed, its integer value outputs. This output tells you nothing about what the integer value represents. It doesn’t even identify the enumerated type to which the constant belongs.

You could avoid the “lack of type safety” and “not enough information” problems by using java.lang.String constants. For example, you might specify static final String DIR_NORTH = "NORTH";. Although the constant value is more meaningful, String-based constants still suffer from “namespace not present” and brittleness problems. Also, unlike integer comparisons, you cannot compare string values with the == and != operators (which only compare references).

To resolve these problems, developers invented a class-based alternative known as typesafe enum. Unfortunately, this pattern had numerous challenges. I’ll show an example that summarizes the issues.

Challenges of the typesafe enum pattern

The Suit class shows how you might use the typesafe enum pattern to introduce an enumerated type describing the four card suits:


public final class Suit // Should not be able to subclass Suit.
{
   public static final Suit CLUBS = new Suit();
   public static final Suit DIAMONDS = new Suit();
   public static final Suit HEARTS = new Suit();
   public static final Suit SPADES = new Suit();
   private Suit() {} // Should not be able to introduce additional constants.
}

To use this class, you would introduce a Suit variable and assign it to one of Suit’s constants:


Suit suit = Suit.DIAMONDS;

You might then want to interrogate suit in a switch statement like this one:


switch (suit)
{
   case Suit.CLUBS   : System.out.println("clubs"); break;
   case Suit.DIAMONDS: System.out.println("diamonds"); break;
   case Suit.HEARTS  : System.out.println("hearts"); break;
   case Suit.SPADES  : System.out.println("spades");
}

However, when the Java compiler encounters Suit.CLUBS, it reports an error stating that a constant expression is required. You might try to address the problem as follows:


switch (suit)
{
   case CLUBS   : System.out.println("clubs"); break;
   case DIAMONDS: System.out.println("diamonds"); break;
   case HEARTS  : System.out.println("hearts"); break;
   case SPADES  : System.out.println("spades");
}

But when the compiler encounters CLUBS, it will report an error stating that it was unable to find the symbol. And even if you placed Suit in a package, imported the package, and statically imported these constants, the compiler would complain that it cannot convert Suit to int when encountering suit in switch(suit). Regarding each case, the compiler would also report that a constant expression is required.

Java doesn’t support the typesafe enum pattern with switch statements. However, you can use the typesafe enum language feature, which encapsulates the pattern’s benefits while resolving its issues. This feature also supports switch.

How to use typesafe enums in switch statements

A simple typesafe enum declaration in Java code looks like its counterparts in the C, C++, and C# languages:


enum Direction { NORTH, WEST, EAST, SOUTH }

This declaration uses the keyword enum to introduce Direction as a typesafe enum (a special kind of class), in which arbitrary methods can be added and arbitrary interfaces can be implemented. The NORTH, WEST, EAST, and SOUTH enum constants are implemented as constant-specific class bodies that define anonymous classes extending the enclosing Direction class.

Direction and other typesafe enums extend Enum<E extends Enum<E>> and inherit various methods, including values(), toString(), and compareTo(), from this class. We’ll explore Enum later in this article.

Listing 1 declares the aforementioned enum and uses it in a switch statement. It also shows how to compare two enum constants, to determine which constant comes before the other constant.

Listing 1. TEDemo.java (version 1)


public class TEDemo
{
   enum Direction { NORTH, WEST, EAST, SOUTH }
   public static void main(String[] args)
   {
      for (int i = 0; i < Direction.values().length; i++)
      {
         Direction d = Direction.values()[i];
         System.out.println(d);
         switch (d)
         {
            case NORTH: System.out.println("Move north"); break;
            case WEST : System.out.println("Move west"); break;
            case EAST : System.out.println("Move east"); break;
            case SOUTH: System.out.println("Move south"); break;
            default   : assert false: "unknown direction";
         }
      }
      System.out.println(Direction.NORTH.compareTo(Direction.SOUTH));
   }
}

Listing 1 declares the Direction typesafe enum and iterates over its constant members, which values() returns. For each value, the switch statement (enhanced to support typesafe enums) chooses the case that corresponds to the value of d and outputs an appropriate message. (You don’t prefix an enum constant, e.g., NORTH, with its enum type.) Lastly, Listing 1 evaluates Direction.NORTH.compareTo(Direction.SOUTH) to determine if NORTH comes before SOUTH.

Compile the source code as follows:

javac TEDemo.java

Run the compiled application as follows:

java TEDemo

You should observe the following output:

NORTH
Move north
WEST
Move west
EAST
Move east
SOUTH
Move south
-3

The output reveals that the inherited toString() method returns the name of the enum constant, and that NORTH comes before SOUTH in a comparison of these enum constants.

How to add data and behaviors in typesafe enums

You can add data (in the form of fields) and behaviors (in the form of methods) to a typesafe enum. For example, suppose you need to introduce an enum for Canadian coins, and you want the class to provide the means to return the number of nickels, dimes, quarters, or dollars contained in an arbitrary number of pennies. Listing 2 shows you how to accomplish this task.

Listing 2. TEDemo.java (version 2)


enum Coin
{
   NICKEL(5),   // constants must appear first
   DIME(10),
   QUARTER(25),
   DOLLAR(100); // the semicolon is required
   private final int valueInPennies;
   Coin(int valueInPennies)
   {
      this.valueInPennies = valueInPennies;
   }
   int toCoins(int pennies)
   {
      return pennies / valueInPennies;
   }
}
public class TEDemo
{
   public static void main(String[] args)
   {
      if (args.length != 1)
      {
          System.err.println("usage: java TEDemo amountInPennies");
          return;
      }
      int pennies = Integer.parseInt(args[0]);
      for (int i = 0; i < Coin.values().length; i++)
           System.out.println(pennies + " pennies contains " +
                              Coin.values()[i].toCoins(pennies) + " " +
                              Coin.values()[i].toString().toLowerCase() + "s");
   }
}

Listing 2 first declares a Coin enum. A list of parameterized constants identifies four kinds of coins. The argument passed to each constant represents the number of pennies that the coin represents.

The argument passed to each constant is passed to the Coin(int valueInPennies) constructor, which saves the argument in the valuesInPennies instance field. This variable is accessed from within the toCoins() instance method. It divides into the number of pennies passed to toCoin()’s pennies parameter, and this method returns the result, which happens to be the number of coins in the monetary denomination described by the Coin constant.

At this point, you’ve discovered that you can declare instance fields, constructors, and instance methods in a typesafe enum. After all, a typesafe enum is essentially a special kind of Java class.

The TEDemo class’s main() method first verifies that a single command-line argument has been specified. This argument is converted to an integer by calling the java.lang.Integer class’s parseInt() method, which parses the value of its string argument into an integer (or throws an exception when invalid input is detected).

Moving forward, main() iterates over Coin’s constants. Because these constants are stored in a Coin[] array, main() evaluates Coin.values().length to determine the length of this array. For each iteration of loop index i, main() evaluates Coin.values()[i] to access the Coin constant. It invokes each of toCoins() and toString() on this constant, which further proves that Coin is a special kind of class.

Compile the source code as follows:

javac TEDemo.java

Run the compiled application as follows:

java TEDemo 198

You should observe the following output:


198 pennies contains 39 nickels
198 pennies contains 19 dimes
198 pennies contains 7 quarters
198 pennies contains 1 dollars

What you need to know about Enum (Enum<E extends Enum<E>>)

The Java compiler considers enum to be syntactic sugar. Upon encountering a typesafe enum declaration, it generates a class whose name is specified by the declaration. This class subclasses the abstract Enum<E extends Enum<E>> class, which serves as the base class for all typesafe enums.

Enum’s formal type parameter list looks ghastly, but it’s not that hard to understand. For example, in the context of Coin extends Enum<Coin>, you would interpret this formal type parameter list as follows:

  • Any subclass of Enum must supply an actual type argument to Enum. For example, Coin’s header specifies Enum<Coin>.
  • The actual type argument must be a subclass of Enum. For example, Coin is a subclass of Enum.
  • A subclass of Enum (such as Coin) must follow the idiom that it supplies its own name (Coin) as an actual type argument.

Examine Enum’s Java documentation and you’ll discover that it overrides java.lang.Object‘s clone(), equals(), finalize(), hashCode(), and toString() methods. Except for toString(), all of these overriding methods are declared final so that they cannot be overridden in a subclass:

  • clone() is overridden to prevent constants from being cloned so that there is never more than one copy of a constant; otherwise, constants could not be compared via == and !=.
  • equals() is overridden to compare constants via their references. Constants with the same identities (==) must have the same contents (equals()), and different identities imply different contents.
  • finalize() is overridden to ensure that constants cannot be finalized.
  • hashCode() is overridden because equals() is overridden.
  • toString() is overridden to return the constant’s name.

Enum also provides its own methods. These methods include the final compareTo() (Enum implements the java.lang.Comparable<T> interface), getDeclaringClass(), name(), and ordinal() methods:

  • compareTo() compares the current constant with the constant passed as an argument to see which constant precedes the other constant in the enum and returns a value indicating their order. This method makes it possible to sort an array of unsorted constants.
  • getDeclaringClass() returns the java.lang.Class object corresponding to the current constant’s enum. For example, the Class object for Coin, in enum Coin { PENNY, NICKEL, DIME, QUARTER}, is returned when calling Coin.PENNY.getDeclaringClass().
  • name() returns the constant’s name. Unless overridden to return something more descriptive, toString() also returns the constant’s name.
  • ordinal() returns a zero-based ordinal, an integer that identifies the constant’s position in the typesafe enum. This integer is used by compareTo(). For example, in the previous Direction.NORTH.compareTo(Direction.SOUTH) expression, compareTo() returned -3 because NORTH has ordinal 0, SOUTH has ordinal 3, and 0 – 3 (which is what compareTo() in this context evaluates) equals -3.

Enum also provides the public static <T extends Enum<T>> T valueOf(Class<T> enumType, String name) method for returning the constant from the specified typesafe enum with the specified name:

  • enumType identifies the Class object of the enum from which to return a constant.
  • name identifies the name of the constant to return.

For example, Coin penny = Enum.valueOf(Coin.class, "PENNY"); assigns the Coin constant whose name is PENNY to penny.

Finally, Enum’s Java documentation doesn’t include a values() method because the compiler manufactures this method while generating the typesafe enum’s class file.

Conclusion

You should get into the habit of using typesafe enums instead of traditional enumerated types. It’s also a good idea to use lambdas instead of anonymous inner classes.

Exit mobile version