Prior to
release 1.5, it was common to use naming patterns to indicate
that some program elements demanded special treatment by a tool or framework.
For example, the JUnit testing framework originally required its users to
designate test methods by beginning their names with the characters test [Beck04]. This technique works, but it has
several big disadvantages.
First,
typographical errors may result in silent failures. For example, suppose you
accidentally name a test method tsetSafetyOverride
instead
of testSafetyOverride.
A second
disadvantage of naming patterns is that there is no way to ensure that they are
used only on appropriate program elements. For example, suppose you call a
class testSafetyMechanisms in hopes that
JUnit will automatically test all of its methods, regardless of their names.
Again, JUnit won’t complain, but it won’t execute the tests either.
A third
disadvantage of naming patterns is that they provide no good way to associate
parameter values with program elements. For example, suppose you want to
support a category of test that succeeds only if it throws a particular
exception. The exception type is essentially a parameter of the test. You could
encode the exception type name into the test method name using some elaborate
naming pattern, but this would be ugly and fragile (Item 50).
Annotations
[JLS, 9.7] solve all of these problems nicely. Suppose you want to define an
annotation type to designate simple tests that are run automatically and fail
if they throw an exception. Here’s how such an annotation type, named Test, might look:
// Marker annotation type declaration
import java.lang.annotation.*;
/**
* Indicates that the annotated method is a test method.
* Use only on parameterless static methods.
*/
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface Test {
}
The
declaration for the Test annotation type is itself
annotated with Retention and Target annotations. Such annotations on annotation
type declarations are known as meta-annotations. The @Retention(RetentionPolicy.RUNTIME) meta-annotation
indicates that Test annotations should be retained at
runtime. Without it, Test annotations would be invisible to
the test tool. The @Target(
ElementType.METHOD) meta-annotation indicates that
the Test annotation is legal only on
method declarations: it cannot be applied to class declarations, field
declarations, or other program elements.
If you put a Test annotation on the declaration of an instance
method or a method with one or more parameters, the test program will still
compile, leaving it to the testing tool to deal with the problem at runtime.
Here is how
the Test annotation looks in practice. It
is called a marker annotation, because it
has no parameters but simply “marks” the annotated element. If the programmer
were to misspell Test, or to apply the Test annotation to a program element other than a
method declaration, the program wouldn’t compile:
// Program containing marker annotations
public class Sample {
@Test public
static void m1() { } // Test should pass
public static void m2() { }
@Test public
static void m3() { // Test Should fail
throw new RuntimeException("Boom");
}
public static void m4() { }
@Test public
void m5() { } // INVALID USE: nonstatic
method
public static void m6() { }
@Test public
static void m7() { // Test should fail
throw new RuntimeException("Crash");
}
public static void m8() { }
}
The Sample class has eight static methods, four of which
are annotated as tests. Two of these, m3
and
m7, throw exceptions and two, m1 and m5, do not. But
one of the annotated methods that does not throw an exception, m5, is an instance method, so it is not a valid
use of the annotation. In sum, Sample contains four
tests: one will pass, two will fail, and one is invalid. The four methods that
are not annotated with the Test annotation
will be ignored by the testing tool.
More
generally, annotations never change the semantics of the annotated code, but
enable it for special treatment by tools such as this simple test runner:
// Program to process marker annotations
import java.lang.reflect.*;
public class RunTests {
public static void main(String[] args) throws Exception
{
int tests = 0;
int passed = 0;
Class testClass = Class.forName(args[0]);
for (Method m : testClass.getDeclaredMethods()) {
if (m.isAnnotationPresent(Test.class)) {
tests++;
try {
m.invoke(null);
passed++;
} catch (InvocationTargetException wrappedExc) {
Throwable exc = wrappedExc.getCause();
System.out.println(m + " failed: " + exc);
} catch (Exception exc) {
System.out.println("INVALID @Test: " + m);
}
}
}
System.out.printf("Passed: %d, Failed: %d%n",
passed, tests - passed);
}
}
The isAnnotationPresent method tells the tool which
methods to run. If a test method throws an exception, the reflection facility
wraps it in an InvocationTargetException. The tool
catches this exception and prints a failure report containing the original
exception thrown by the test method, which is extracted from the InvocationTargetException with the getCause method.
Here is the
output that is printed if Run- Tests is run on Sample:
public static void Sample.m3() failed: RuntimeException:
Boom
INVALID @Test: public void Sample.m5()
public static void Sample.m7() failed: RuntimeException:
Crash
Passed: 1, Failed: 3
Now let’s add
support for tests that succeed only if they throw a particular exception. We’ll
need a new annotation type for this:
// Annotation type with a parameter
import java.lang.annotation.*;
/**
* Indicates that the annotated method is a test method
that
* must throw the designated exception to succeed.
*/
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface ExceptionTest {
Class<? extends Exception> value();
}
Here’s how the
annotation looks in practice. Note that class literals are used as the values
for the annotation parameter:
// Program containing annotations with a parameter
public class Sample2 {
@ExceptionTest(ArithmeticException.class)
public static void m1() { // Test should pass
int i = 0;
i = i / i;
}
@ExceptionTest(ArithmeticException.class)
public static void m2() { // Should fail (wrong
exception)
int[] a = new int[0];
int i = a[1];
}
@ExceptionTest(ArithmeticException.class)
public static void m3() { } // Should fail (no
exception)
}
Now let’s
modify the test runner tool to process the new annotation. Doing so consists of
adding the following code to the main
method:
if (m.isAnnotationPresent(ExceptionTest.class)) {
tests++;
try {
m.invoke(null);
System.out.printf("Test %s failed: no exception%n",
m);
} catch (InvocationTargetException wrappedEx) {
Throwable exc = wrappedEx.getCause();
Class<? extends Exception> excType =
m.getAnnotation(ExceptionTest.class).value();
if (excType.isInstance(exc)) {
passed++;
} else {
System.out.printf(
"Test %s failed: expected %s, got %s%n",
m, excType.getName(), exc);
}
} catch (Exception exc) {
System.out.println("INVALID @Test: " + m);
}
}
This code is
similar to the code we used to process Test
annotations,
with one exception: this code extracts the value of the annotation parameter
and uses it to check if the exception thrown by the test is of the right type.
There are no explicit casts, hence no danger of a ClassCastException.
Suppose we
change the parameter type of the ExceptionTest
annotation
to be an array of Class objects:
// Annotation type with an array parameter
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface ExceptionTest {
Class<? extends Exception>[] value();
}
The syntax for
array parameters in annotations is flexible. It is optimized for single-element
arrays. All of the previous ExceptionTest
annotations
are still valid with the new array-parameter version of ExceptionTest and result in single- element arrays. To
specify a multiple-element array, surround the elements with curly braces and
separate them with commas:
// Code containing an annotation with an array
parameter
@ExceptionTest({
IndexOutOfBoundsException.class,
NullPointerException.class })
public static void doublyBad() {
List<String> list = new ArrayList<String>();
// The spec permits this method to throw either
// IndexOutOfBoundsException or NullPointerException
list.addAll(5, null);
}
It is
reasonably straightforward to modify the test runner tool to process the new
version of ExceptionTest. This code
replaces the original version:
if (m.isAnnotationPresent(ExceptionTest.class)) {
tests++;
try {
m.invoke(null);
System.out.printf("Test %s failed: no
exception%n", m);
} catch (Throwable wrappedExc) {
Throwable exc = wrappedExc.getCause();
Class<? extends Exception>[] excTypes
=
m.getAnnotation(ExceptionTest.class).value();
int oldPassed = passed;
for (Class<? extends Exception> excType :
excTypes) {
if (excType.isInstance(exc)) {
passed++;
break;
}
}
if (passed == oldPassed)
System.out.printf("Test %s failed: %s %n",
m, exc);
}
}
The testing
framework developed in this item is just a toy, but it clearly demonstrates the
superiority of annotations over naming patterns. And it only scratches the
surface of what you can do with annotations. If you write a tool that requires
programmers to add information to source files, define an appropriate set of
annotation types. There is simply no reason to use naming patterns now that we have
annotations.
That said,
with the exception of toolsmiths, most programmers will have no need to define
annotation types. All programmers should, however, use the predefined annotation types provided by
the Java platform (Items 36 and Item 24). Also, consider using any annotations
provided by your IDE or static analysis tools. Such annotations can improve the
quality of the diagnostic information provided by these tools. Note, however,
that these annotations have yet to be standardized, so you will have some work
to do if you switch tools, or if a standard emerges.
Reference: Effective Java 2nd Edition by Joshua Bloch