Java 101: The next generation: The essential Java language features tour, Part 2

Programming with typesafe enums and annotations in Java 5

Get the scoop on typesafe enums and learn how to use them properly in switch statements, then get started with Java annotations and meta-annotations types like Target, which you can use to clarify the purpose and function of annotations in your Java code.

The first article in my Java language evolution tour introduced assertions and generics, concluding with a discussion about why the generics suite was a particularly controversial addition to Java 5. Here I introduce typesafe enums and annotations, two more Java 5 language features designed to enhance the safety and productivity of Java programs.

Typesafe enums

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

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:

  • Lack of 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 (e.g., (DIR_NORTH+DIR_EAST)/DIR_SOUTH), which is meaningless.
  • Namespace not 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.
  • Brittleness: Because enumerated type constants are compiled into classfiles where their literal values are stored (in constant pools), changing a constant's value requires that these classfiles and those application classfiles that depend on them be rebuilt. Otherwise, undefined behavior will occur at runtime.
  • Lack of 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.

Recognizing the problems with the traditional implementation of enumerated types, developers invented a class-based alternative known as the Typesafe Enum pattern. This pattern has been widely described and critiqued. Joshua Bloch introduced the pattern in Item 21 of his Effective Java Programming Language Guide (Addison-Wesley, 2001) and noted that it has some problems; namely that it is awkward to aggregate typesafe enum constants into sets, and that enumeration constants can't be used in switch statements.

To put the Typesafe Enum pattern in perspective, consider the following code snip. The Suit class declaration shows how you might use the class-based alternative to introduce an enumerated type that describes the four card suits (clubs, diamonds, hearts, and spades):


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, as follows:


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");
}

When encountering Suit.CLUBS, the compiler would report an error stating that a constant expression was 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");
}

When it encountered CLUBS, however, the compiler would report an error stating that it was unable to find the symbol.

The typesafe enum pattern doesn't work for switch statements. Instead, you're better off using the typesafe enum language feature introduced in Java 5, which encapsulates the pattern's benefits while resolving its issues.

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 keyword enum to define Direction as an enum 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 enum classes extend the primordial enum class: java.lang.Enum<E extends Enum<E>>. Enum classes inherit various methods from java.lang.Enum<E extends Enum<E>>, including high-quality implementations of java.lang.Object methods like public String toString(). Also, the Java compiler automatically generates several members for Direction (e.g., a values() method).

Note that you can also compare enum constants to other constants of the same enumerated type according to the order in which the constants are declared. You can do this because Enum<E extends Enum<E>> implements the java.lang.Comparable<T> interface.

Listing 1 declares and uses the aforementioned enum in an application context.

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 enum class and iterates over this class's constant members, which values() returns. For each value, the switch statement (enhanced to support typesafe enums) chooses the case that corresponds to the value expressed by d and outputs an appropriate message. (You do not prefix an enum constant [e.g., NORTH] with its enum type.)

If you compile Listing 1 (javac TEDemo.java) and run the application (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.

Adding data and behavior to a typesafe enum

You can add data and behavior to a typesafe enum. For example, suppose you need to introduce an enum for Canadian coins, and that this class must 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 (Coin coin: Coin.values())
           System.out.println(pennies+" pennies contains "+
                              coin.toCoins(pennies)+" "+
                              coin.toString().toLowerCase()+"s");
   }
}

The Coin enum in Listing 2 declares a constructor that takes the number of pennies represented by a specific coin as an argument. This value is used to find out how many coins of a specific type are contained in an arbitrary number of pennies via the class's int toCoins(int pennies) method. For example, if you invoked java TEDemo 198, you would observe this output:


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

Note that Listing 2 demonstrates Java 5's enhanced for loop: for (Coin coin: Coin.values()). This feature conveniently returns each Coin instance from the array that values() returns and assigns it to coin. This instance is then accessed in the body of the for loop. I'll have more to say about the enhanced for loop in Part 3.

This has been a quick introduction to using typesafe enums in switch statements and adding data and behavior to typesafe enums. You can learn more about using typesafe enums with the Java Collections Framework by reading the Oracle JDK 5.0 Enum documentation. Dustin Marx has also written a useful introduction to using Java enums for unit conversions, which you can read on JavaWorld.

Annotations

You've probably encountered the need to annotate elements of your Java applications by associating metadata (data that describes other data) with them. Java has always provided an ad hoc annotation mechanism via the transient reserved word, which lets you annotate fields that are to be excluded during serialization. But it didn't offer a standard way to annotate program elements until Java 5.

Java 5's general annotation mechanism consists of four components:

  1. An @interface mechanism for declaring annotation types.
  2. Meta-annotation types, which you can use to identify the application elements to which an annotation type applies; to identify the lifetime of an annotation (an instance of an annotation type); and more.
  3. Support for annotation processing via an extension to the Java Reflection API, which you can use to discover a program's runtime annotations, and the introduction of a generalized tool for processing annotations.
  4. Standard annotation types.

I'll explain how to use these components and point out some of the challenges of annotations in the examples that follow.

Declaring annotation types with @interface

You can declare an annotation type by specifying the @ symbol immediately followed by the interface reserved word and an identifier. For example, Listing 3 declares a simple annotation type that you might use to annotate thread-safe code.

Listing 3. ThreadSafe.java


public @interface ThreadSafe
{
}

After declaring this annotation type, prefix the methods that you consider thread-safe with instances of this type by prepending @ immediately followed by the type name to the method headers. Listing 4 offers a simple example where the main() method is annotated with @ThreadSafe.

Listing 4. AnnDemo.java (version 1)


public class AnnDemo
{
   @ThreadSafe
   public static void main(String[] args)
   {
   }
}

ThreadSafe instances supply no metadata other than the annotation type name. However, you can supply metadata by adding elements to this type, where an element is a method header placed in the annotation type's body.

As well as not having code bodies, elements are subject to the following restrictions:

  • The method header cannot declare parameters.
  • The method header cannot provide a throws clause.
  • The method header's return type must be a primitive type (e.g., int), java.lang.String, java.lang.Class, an enum, an annotation type, or an array of one of these types. No other type can be specified for the return type.

As another example, Listing 5 presents a ToDo annotation type with three elements identifying a particular coding job, specifying the date when the job is to be finished, and naming the coder responsible for completing the job.

Listing 5. ToDo.java (version 1)


public @interface ToDo
{
   int id();
   String finishDate();
   String coder() default "n/a";
}

Note that each element declares no parameter(s) or throws clause, has a legal return type (int or String), and terminates with a semicolon. Also, the final element reveals that a default return value can be specified; this value is returned when an annotation doesn't assign a value to the element.

Listing 6 uses ToDo to annotate an unfinished class method.

Listing 6. AnnDemo.java (version 2)


public class AnnDemo
{
   public static void main(String[] args)
   {
      String[] cities = { "New York", "Melbourne", "Beijing", "Moscow",
                          "Paris", "London" };
      sort(cities);
   }
   @ToDo(id=1000, finishDate="10/10/2013", coder="John Doe")
   static void sort(Object[] objects)
   {
   }
}
1 2 Page
Join the discussion
Be the first to comment on this article. Our Commenting Policies
See more