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)
   {
   }
}

Listing 6 assigns a metadata item to each element; for example, 1000 is assigned to id. Unlike coder, the id and finishDate elements must be specified; otherwise, the compiler will report an error. When coder isn't assigned a value, it assumes its default "n/a" value.

Java provides a special String value() element that can be used to return a comma-separated list of metadata items. Listing 7 demonstrates this element in a refactored version of ToDo.

Listing 7. ToDo.java (version 2)


public @interface ToDo
{
   String value();
}

When value() is an annotation type's only element, you don't have to specify value and the = assignment operator when assigning a string to this element. Listing 8 demonstrates both approaches.

Listing 8. AnnDemo.java (version 3)


public class AnnDemo
{
   public static void main(String[] args)
   {
      String[] cities = { "New York", "Melbourne", "Beijing", "Moscow",
                          "Paris", "London" };
      sort(cities);
   }
   @ToDo(value="1000,10/10/2013,John Doe")
   static void sort(Object[] objects)
   {
   }
   @ToDo("1000,10/10/2013,John Doe")
   static boolean search(Object[] objects, Object key)
   {
      return false;
   }
}

Using meta-annotation types -- the problem of flexibility

You can annotate types (e.g., classes), methods, local variables, and more. However, this flexibility can be problematic. For example, you might want to restrict ToDo to methods only, but nothing prevents it from being used to annotate other application elements, as demonstrated in Listing 9.

Listing 9. AnnDemo.java (version 4)


@ToDo("1000,10/10/2013,John Doe")
public class AnnDemo
{
   public static void main(String[] args)
   {
      @ToDo(value="1000,10/10/2013,John Doe")
      String[] cities = { "New York", "Melbourne", "Beijing", "Moscow",
                          "Paris", "London" };
      sort(cities);
   }
   @ToDo(value="1000,10/10/2013,John Doe")
   static void sort(Object[] objects)
   {
   }
   @ToDo("1000,10/10/2013,John Doe")
   static boolean search(Object[] objects, Object key)
   {
      return false;
   }
}

In Listing 9, ToDo is also used to annotate the AnnDemo class and the cities local variable. The presence of these erroneous annotations might confuse someone reviewing your code, or even your own annotation-processing tools. For the times when you need to narrow an annotation type's flexibility, Java offers the Target annotation type in its java.lang.annotation package.

Target is a meta-annotation type that identifies the kinds of program elements to which an annotation type is applicable. These elements are identified by Target's ElementValue[] value() element.

java.lang.annotation.ElementType is an enum whose constants describe program elements. For example, CONSTRUCTOR applies to constructors and PARAMETER applies to parameters. Listing 10 refactors Listing 7's ToDo annotation type to restrict it to methods only.

Listing 10. ToDo.java (version 3)


import java.lang.annotation.ElementType;
import java.lang.annotation.Target;
@Target({ElementType.METHOD})
public @interface ToDo
{
   String value();
}

Given the refactored ToDo annotation type, an attempt to compile Listing 9 would now result in the following error message:


AnnDemo.java:1: error: annotation type not applicable to this kind of
declaration
@ToDo("1000,10/10/2013,John Doe")
^
AnnDemo.java:6: error: annotation type not applicable to this kind of
declaration
      @ToDo(value="1000,10/10/2013,John Doe")
      ^
2 errors

Additional meta-annotation types

Java 5 supports three additional meta-annotation types, which are found in the java.lang.annotation package:

  • Retention indicates how long annotations with the annotated type are to be retained. This type's associated java.lang.annotation.RetentionPolicy enum declares constants CLASS (compiler records annotations in classfile; virtual machine doesn't retain them to save memory -- default policy), RUNTIME (compiler records annotations in classfile; virtual machine retains them), and SOURCE (compiler discards annotations).
  • Documented indicates that instances of Documented-annotated annotations are to be documented by javadoc and similar tools.
  • Inherited indicates that an annotation type is automatically inherited.

Processing annotations

Annotations are meant to be processed; otherwise, there's no point in having them. Java 5 extended the Reflection API to help you create your own annotation-processing tools. For example, Class declares an Annotation[] getAnnotations() method that returns an array of java.lang.Annotation instances describing annotations present on the element described by the Class object.

Listing 11 presents a simple application that loads a classfile, interrogates its methods for ToDo annotations, and outputs the components of each found annotation.

Listing 11. AnnProcDemo.java


import java.lang.reflect.Method;
public class AnnProcDemo
{
   public static void main(String[] args) throws Exception
   {
      if (args.length != 1)
      {
         System.err.println("usage: java AnnProcDemo classfile");
         return;
      }
      Method[] methods = Class.forName(args[0]).getMethods();
      for (int i = 0; i < methods.length; i++)
      {
         if (methods[i].isAnnotationPresent(ToDo.class))
         {
            ToDo todo = methods[i].getAnnotation(ToDo.class);
            String[] components = todo.value().split(",");
            System.out.printf("ID = %s%n", components[0]);
            System.out.printf("Finish date = %s%n", components[1]);
            System.out.printf("Coder = %s%n%n", components[2]);
         }
      }
   }
}

After verifying that exactly one command-line argument (identifying a classfile) has been specified, main() loads the classfile via Class.forName(), invokes getMethods() to return an array of java.lang.reflect.Method objects identifying all public methods in the classfile, and processes these methods.

Method processing begins by invoking Method's boolean isAnnotationPresent(Class<? extends Annotation> annotationClass) method to determine if the annotation described by ToDo.class is present on the method. If so, Method's <T extends Annotation> T getAnnotation(Class<T> annotationClass) method is called to obtain the annotation.

The ToDo annotations that are processed are those whose types declare a single String value() element (see Listing 7). Because this element's string-based metadata is comma-separated, it needs to be split into an array of component values. Each of the three component values is then accessed and output.

Compile this source code (javac AnnProcDemo.java). Before you can run the application, you'll need a suitable classfile with @ToDo annotations on its public methods. For example, you could modify Listing 8's AnnDemo source code to include public in its sort() and search() method headers. You'll also need Listing 12's ToDo annotation type, which requires the RUNTIME retention policy.

Listing 12. ToDo.java (version 4)


import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface ToDo
{
   String value();
}

Compile the modified AnnDemo.java and Listing 12, and execute the following command to process AnnDemo's ToDo annotations:


java AnnProcDemo AnnDemo

If all goes well, you should observe the following output:


ID = 1000
Finish date = 10/10/2013
Coder = John Doe
ID = 1000
Finish date = 10/10/2013
Coder = John Doe

Standard annotation types

Along with Target, Retention, Documented, and Inherited, Java 5 introduced java.lang.Deprecated, java.lang.Override, and java.lang.SuppressWarnings. These three annotation types are designed to be used in a compiler context only, which is why their retention policies are set to SOURCE.

Deprecated annotates program elements that are to be deprecated (phased out). Such elements should no longer be used. The compiler will warn you when a deprecated element is being accessed.

The following java.util.Date constructor demonstrates Deprecated:


@Deprecated
public Date(int year, int month, int date, int hrs, int min)
{
   // ... body of this constructor
}

Override annotates subclass methods that override their superclass counterparts. The compiler reports an error when the subclass method doesn't override the superclass method.

The following example demonstrates Override where the java.lang.Runnable interface's public void run() method is overridden by an anonymous class:


Runnable r = new Runnable()
             {
                @Override
                public void run()
                {
                   // ... body of this method
                }
             };

SuppressWarnings annotates program elements (and all elements contained in these program elements) where any of the named compiler warnings (e.g., deprecation and unchecked) should be suppressed.

The following example uses SuppressWarnings to suppress an unchecked warning in the context of the (E[]) cast -- the constructor is annotated instead of the expression containing the cast because annotations cannot be applied to expressions:


public class Container<E>
{
   private E[] elements
   // ...
   @SuppressWarnings("unchecked")
   public Container<E>(int size)
   {
      // ...
      elements = (E[]) new Object[size];
   }
   // ...
}

Be careful when suppressing an unchecked warning. First prove that a ClassCastException cannot be thrown and then provide the appropriate @SuppressWarnings("unchecked") annotation.

In conclusion

Like generics, typesafe enums did much to improve the safety of Java programs, while annotations were aimed at increasing developer productivity. This article has been a brief tour of essential use cases for each feature. The next article in this series will introduce the remaining language features introduced in JDK 5 -- namely, autoboxing and unboxing, the enhanced for loop, static imports, varargs, and covariant return types.

Join the discussion
Be the first to comment on this article. Our Commenting Policies