Annotations in J2SE 5.0—Part 1

Java version 5.0 introduces many dramatic new language features. This article introduces the new annotations facility.

Annotations and Annotation Types

An annotation is a tag you can insert into your source code. It does not alter the semantics of your code, but instead adds the ability for other application code to recognize and interpret the tag. An example of an annotation you are familiar with is the @deprecated tag1.

You mark methods with @deprecated to indicate that they should no longer be used. An annotation is just a note. Marking a method with @deprecated does not change how that method works. However, the compiler notes the methods marked with @deprecation and issues a warning when any client code calls these methods.

J2SE 5.0 allows you to add custom annotations to your code. As an example, you might want to be able to mark bits of code as @Todo, meaning that work remains to be done on that code. To support using an @Todo tag, you code an annotation type definition in Todo.java (you'll see what this looks like shortly).

Once you've compiled the annotation type, you can use it in your source code. Just like any Java type, the annotation type must be available on the classpath. You can then build a tool that uses reflection code to extract the annotation information from the code.

The @Override Annotation Type

As one example of practical use, Sun included a new standard annotation type, @Override, in J2SE 5.0. This annotation type, defined in java.lang, allows you to declare that you think a method overrides, and does not overload, a method from the superclass.

public @Override boolean equals(Employee that)
In this example, the developer likely intended to override the method:
public boolean equals(Object o)
The compiler will fail if there is no method with an equivalent signature in the superclass:
method does not override a method from its superclass
public @Override boolean equals(String o) {
        ^

Example: Ignoring Test Methods

I use JUnit heavily for building unit tests for my code. JUnit depends upon test methods being named according to a convention: all test methods should begin with the name "test", should return no value, and should have no parameters. JUnit uses Java's reflection capabilities to find and execute these test methods.

One of the things I often do when coding with JUnit is to temporarily comment out one or more test methods while I work on another. (One technique I use is to rename the method xxxtestMethod instead of testMethod.) Perhaps the commented-out method is causing additional problems. In any case, I always intend to go back to the method and uncomment it, but I sometimes forget where the method was, or even that I commented out any methods.

It would be nice if I could annotate methods with a tag that tells JUnit to not run them. (NUnit, the corresponding test tool for .Net, provides this capability.) It would be even nicer if JUnit were able to list the methods that were marked as ignored. As a brief example, I will step you through the first part of the changes needed to accomplish these goals.

Step 1—Readying JUnit

Note: The example here uses JUnit 3.8.1.

The JUnit distribution includes the source for JUnit. Unzip the source (src.jar) into the same directory in which you installed JUnit (make sure the option to "Use folder names" is checked).

Step 2—Writing the Test

Next, you need to create a new test class in the junit.tests.framework package. This test class will demonstrate use of the @Ignore tag.

package junit.tests.framework;

/**
 * Test class used in SuiteTest
 */
import junit.framework.TestCase;
import junit.framework.Ignore;

public class IgnoreMethodTestCase extends TestCase {
   @Ignore
   public void testCase() {
   }

   public void testNotIgnored() {
   }
}
The test class IgnoreMethodTestCase contains two test methods. One is marked with the @Ignore annotation; the other is not. Now that you have created this test class, you can add a new test method to JUnit. This new test method will be used to demonstrate that the @Ignore annotation works as expected.

Here is the new test method; add it to junit.tests.framework.SuiteTest:

public void testIgnoreMethodTestCase() {
   TestSuite suite= new TestSuite();
   suite.addTestSuite(IgnoreMethodTestCase.class);
   suite.run(fResult);
   assertEquals(1, fResult.runCount());
   assertTrue(fResult.wasSuccessful());
}
This new test looks like most of the other existing tests in SuiteTest. It first constructs a TestSuite, then adds your IgnoreMethodTestCase class to the suite. After running the suite, the test expects a successful run with a runCount of 1 instead of 2. In other words, the JUnit code should find only one test method in IgnoreMethodTestCase, since the other test method has been marked with the @Ignore tag.

Execute the JUnit Ant script to demonstrate that the the new code does not compile--you haven't yet created the @Ignore annotation type.

Step 3—Defining the Annotation Type

Here is the code for the custom annotation type @Ignore. Save it as Ignore.java:

package junit.framework;

import java.lang.annotation.*;

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface Ignore {}

The annotation type declaration is preceded with two meta-annotation tags: @Retention and @Target. The "meta" means that these annotations are tags that apply to annotation declarations: annotations for annotations. (The meta-annotation types @Retention and @Target are defined in java.lang.annotation, hence the import statement.) Note that both of the meta-annotations take parameters. You'll see how to define parameters for @Ignore in the next installment of this article.

The @Retention tag indicates that the @Ignore tag will be available at runtime. This means you'll be able to use reflection capabilities to determine if the @Ignore tag has been used.

The RetentionPolicy enum defines two values in addition to RetentionPolicy.RUNTIME. RetentionPolicy.CLASS indicates that the compiler stores the annotation but does not make it available at runtime (this is how Sun declares the @Override annotation). If no @Retention tag is supplied, Java uses RetentionPolicy.CLASS as the default. A third option, RetentionPolicy.SOURCE, means that the Java compiler discards the annotation at compile time; the annotation is only available in the source. An IDE might find such tags useful.

The @Target tag indicates where the annotation can appear. The enum ElementType defines the possibilities. In the @Ignore definition above, ElementType.METHOD means that @Ignore can precede a method declaration. Here is the complete list of enum values:

ElementType enumWhat it can modify
TYPEclass, enum, interface
FIELDfield, enum constant
METHODmethod
PARAMETERparameter
CONSTRUCTORconstructor
LOCAL_VARIABLEtemporary variable
ANNOTATION_TYPEannotation type
PACKAGEpackage

The final line in Ignore.java is the annotation type declaration, repeated here:

public @interface Ignore {}
An annotation type declaration looks just like an interface type declaration. The only distinction is that you precede the name of an annotation type with @ (plus optional whitespace, which is not recommended).

Now execute the JUnit Ant script again. The code should compile but will fail the tests. It fails because there are two test methods in IgnoreMethodTestCase. You haven't yet coded the logic for @Ignore, so JUnit doesn't know to skip one of the IgnoreMethodTestCase methods.

Step 4— Modifying JUnit

Getting JUnit to skip tests marked with @Ignore will require a few small modifications to the class junit.framework.TestSuite:

private void addTestMethod(Method m, Vector names, Class theClass) {
   String name= m.getName();
   if (names.contains(name))
      return;

   if (!isTestMethod(m))
      return;
   if (isIgnore(m))
      return;
   if (!isPublic(m)) {
      addTest(warning("Test method isn't public: "+m.getName()));
      return;
   }

   names.addElement(name);
   addTest(createTest(theClass, name));
}

The TestSuite method addTestMethod now calls a new method named isIgnore. The code for isIgnore is where you determine whether or not an @Ignore annotation has been applied to the Method object:

private boolean isIgnore(Method m) {
   return m.isAnnotationPresent(Ignore.class);
}
All elements that can be annotated--classes, methods, etc.-- support the ability for you to extract annotation information by using the appropriate java.lang.reflect class. To determine whether or not an annotation has been applied to a method, you call isAnnotationPresent on the Method object and pass it the class object representing the annotation type.

Sun has updated the java.lang.reflect classes in J2SE 5.0 to implement AnnotatedElement. AnnotatedElement is a new interface that declares isAnnotationPresent as well as three other methods: getAnnotation(Class), getAnnotations, and getDeclaredAnnotations.

The original conditional logic in addTestMethod is a little confusing, so a bit of restructuring is prudent. You'll need to make an additional minor modification to the existing JUnit code. Change isPublicTestMethod to simply isPublic, and modify the method contents appropriately:

private boolean isPublic(Method m) {
   return Modifier.isPublic(m.getModifiers());
}

Step 5— Rebuilding JUnit

You're almost done. The last step is to rebuild JUnit.jar with the changes.

Executing Ant from within the JUnit install directory will kick off the default target, dist. The dist target will rebuild the entire JUnit distribution, including JUnit.jar and place it in the subdirectory (of the JUnit installation directory) junit3.8.1.

You can now mark your JUnit test methods as @Ignore. Refer to the test class IgnoreMethodTestCase (above) as an example of how to use the @Ignore tag. (That's part of what having unit tests is for!)

As mentioned, the second part of this exercise would be to modify JUnit to report on the ignored tests. I'll leave that exercise up to you.

In the next installment, I'll go into a little more detail on annotations. Additionally, you'll modify the above example to allow @Ignore to take parameters.

Reference: Java Community Process public review document, "JSR-000175: A Metadata Facility for the JavaTM Programming Language," located at http://jcp.org/aboutJava/communityprocess/review/jsr175/index.html

1 @deprecated is supported in a different manner by Java than the new annotations scheme, but it's still semantically an annotation.