How to describe Java code with annotations

how-to
Jul 02, 202411 mins
Core JavaJavaProgramming Languages

Learn how to use Java annotations to associate metadata with classes, methods, and other application elements in your Java programs.

laptop covered with sticky notes
Credit: Pogonici / iStock

There are times when you need to associate metadata, or data that describes data, with classes, methods, or other elements in your Java code. For example, your team might need to identify unfinished classes in a large application. For each unfinished class, the metadata would include the name of the developer responsible for finishing the class and the class’s expected completion date.

Before Java 5, comments were Java’s only flexible mechanism for associating metadata with application elements. But because the compiler ignores them, comments are not available at runtime. And even if they were available, you would have to parse the text to obtain crucial data items. Without standardizing how the data items were specified, they might be impossible to parse.

Java 5 changed everything by introducing annotations, a standard mechanism for associating metadata with various application elements. This tutorial introduces you to Java annotations.

What you’ll learn in this Java tutorial

  • The four elements of a Java annotation
  • Using @interface to declare annotation types
  • Meta-annotation types and the problem of flexibility
  • How to process annotations
  • Java’s pre-defined annotation types
download
Download the source code for examples in this Java tutorial. Created by Jeff Friesen.

Elements of a Java annotation

An annotation in Java consists of four elements:

  • An @interface mechanism for declaring annotation types.
  • Meta-annotation types, which you can use to identify the application elements an annotation type applies to, identify the lifetime of an annotation (an instance of an annotation type), and more.
  • Support for annotation processing via an extension to the Java Reflection API and a generalized tool for processing annotations.
  • Java’s standard (pre-defined) annotation types.

You’ll learn how to use each of these elements in your Java annotations.

Using @interface to declare annotation types

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

Listing 1. ThreadSafe.java


public @interface ThreadSafe
{
}

After declaring this annotation type, you would prefix the methods that you considered thread-safe with instances of this type by adding the @ symbol followed by the type name to the method headers.

Listing 2 shows a simple example where the main() method is annotated @ThreadSafe.

Listing 2. 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 3 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 3. 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 4 uses ToDo to annotate an unfinished class method.

Listing 4. 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/2019", coder = "John Doe")
   static void sort(Object[] objects)
   {
   }
}

Listing 4 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 5 demonstrates this element in a refactored version of ToDo.

Listing 5. 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 6 demonstrates both approaches.

Listing 6. 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/2019,John Doe")
   static void sort(Object[] objects)
   {
   }

   @ToDo("1000,10/10/2019,John Doe")
   static boolean search(Object[] objects, Object key)
   {
      return false;
   }
}

Meta-annotation types and 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 7.

Listing 7. AnnDemo.java (version 4)


@ToDo("1000,10/10/2019,John Doe")
public class AnnDemo
{
   public static void main(String[] args)
   {
      @ToDo(value = "1000,10/10/2019,John Doe")
      String[] cities = { "New York", "Melbourne", "Beijing", "Moscow", 
                          "Paris", "London" };
      sort(cities);
   }

   @ToDo(value = "1000,10/10/2019,John Doe")
   static void sort(Object[] objects)
   {
   }

   @ToDo("1000,10/10/2019,John Doe")
   static boolean search(Object[] objects, Object key)
   {
      return false;
   }
}

In Listing 7, ToDo is also used to annotate the AnnDemo class and 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—an annotation type for annotating annotation types. This is different from a non-meta-annotation type, whose annotations annotate application elements such as classes and methods. The Target annotation type identifies the kinds of application 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 application elements. For example, CONSTRUCTOR applies to constructors and PARAMETER applies to parameters. Listing 8 refactors Listing 5’s ToDo annotation type to restrict it to methods only.

Listing 8. 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 7 now results in the following error message:


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

Additional meta-annotation types

Java 5 introduced three important 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 the following constants:
    • CLASS: The compiler records annotations in a class file and the virtual machine doesn’t retain them. This is the default policy
    • RUNTIME: The compiler records annotations in a class file and the virtual machine retains them.
    • SOURCE: The 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.

Another meta-annotation type, introduced in Java 8, is java.lang.annotation.Repeatable. Repeatable is used to indicate that the annotation type whose declaration it (meta-)annotates is repeatable. In other words, you can apply multiple annotations from the same repeatable annotation type to an application element, as demonstrated here:


@ToDo(value = "1000,10/10/2019,John Doe")
@ToDo(value = "1001,10/10/2019,Kate Doe")
static void sort(Object[] objects)
{
}

This example assumes that ToDo has been annotated with the Repeatable annotation type.

How to process annotations

Annotations are meant to be processed; otherwise, there’s no point in having them. Java 5 extended the Java 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. These instances describe annotations present on the element described by the Class object.

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

Listing 9. 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 class file) has been specified, main() loads the class file via Class.forName(), invokes getMethods() to return an array of java.lang.reflect.Method objects identifying all public methods in the class file, 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 5). 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 class file with @ToDo annotations on its public methods. For example, you could modify Listing 6’s AnnDemo source code to include public in its sort() and search() method headers. You’ll also need Listing 10’s ToDo annotation type, which requires the RUNTIME retention policy:

Listing 10. 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 10, 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/2019
Coder = John Doe

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

Java’s pre-defined annotation types

Along with the Target, Retention, Documented, and Inherited meta-annotation types, Java 5 introduced Deprecated, Override, and SuppressWarnings as pre-defined annotation types. These three annotation types are designed to be used in a compiler context only, so their retention policies are set to SOURCE.

Deprecated

Deprecated annotates application elements that are to be deprecated, meaning they’ll be phased out and should not 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
}

Java 9 introduced a pair of elements into this annotation type:

  • boolean forRemoval: Indicates whether the annotated element is subject to removal in a future version. The default value is false.
  • String since: Returns the version in which the annotated element became deprecated. The version string is in the same format and namespace as the value of the @since javadoc tag. The default value is the empty string.

These elements are used to alert developers that parts of standard Java APIs are slated for removal in a future Java version and are being phased out, respectively. For example, Java 9 used the since element to identify the version in which java.io.FileInputStream’s and java.io.FileOutputStream’s void finalize() methods were first deprecated:


@Deprecated(since="9")
protected void finalize() throws IOException

Java 10 also used the forRemoval element to indicate that these API methods would be removed in a future Java version. These methods were removed in Java 12:


@Deprecated(since="9",forRemoval=true)
protected void finalize() throws IOException

Override

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

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

The following example uses SuppressWarnings to suppress an unchecked warning in the context of the (E[]) cast:


public class Container<E>
{
   private E[] elements;
   // ...

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

Without the @SuppressWarnings("unchecked") annotation, the compiler would generate a warning about an unchecked cast; specifically, (E[]) casting from Object[] to E[].

You might wonder why I annotated the constructor instead of the expression containing the cast. I did this because Java doesn’t allow applying annotations to expressions.