Typesafe enums offer a better alternative to Java's traditional enumerated types. Here's how to use typesafe enums correctly in your Java code.
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
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 toEnum
. For example,Coin
’s header specifiesEnum<Coin>
. - The actual type argument must be a subclass of
Enum
. For example,Coin
is a subclass ofEnum
. - A subclass of
Enum
(such asCoin
) 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 becauseequals()
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 thejava.lang.Class
object corresponding to the current constant’s enum. For example, theClass
object forCoin
, inenum Coin { PENNY, NICKEL, DIME, QUARTER}
, is returned when callingCoin.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 bycompareTo()
. For example, in the previousDirection.NORTH.compareTo(Direction.SOUTH)
expression,compareTo()
returned -3 becauseNORTH
has ordinal 0,SOUTH
has ordinal 3, and 0 – 3 (which is whatcompareTo()
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 theClass
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.