JSR 308 Explained: Java Type Annotations

by Josh Juneau

The benefits of type annotations and example use cases

Annotations were introduced into Java in Java SE 5, providing a form of syntactic metadata that can be added to program constructs. Annotations can be processed at compile time, and they have no direct effect on the operation of the code. However, they have many use cases. For instance, they can produce informational messages for the developer at compile time, detecting errors or suppressing warnings. In addition, annotations can be processed to generate Java source files or resources that can be used to modify annotated code. The latter is useful for helping to cut down on the amount of configuration resources that must be maintained by an application.

  • Originally published in the March/April 2014 issue of Java Magazine. Subscribe today.

JSR 308, Annotations on Java Types, has been incorporated as part of Java SE 8. This JSR builds upon the existing annotation framework, allowing type annotations to become part of the language. Beginning in Java SE 8, annotations can be applied to types in addition to all of their existing uses within Java declarations. This means annotations can now be applied anywhere a type is specified, including during class instance creation, type casting, the implementation of interfaces, and the specification of throws clauses. This allows developers to apply the benefits of annotations in even more places.

Annotations on types can be useful in many cases, most notably to enforce stronger typing, which can help reduce the number of errors within code. Compiler checkers can be written to verify annotated code, enforcing rules by generating compiler warnings when code does not meet certain requirements. Java SE 8 does not provide a default type-checking framework, but it is possible to write custom annotations and processors for type checking. There are also a number of type-checking frameworks that can be downloaded, which can be used as plug-ins to the Java compiler to check and enforce types that have been annotated. Type-checking frameworks comprise type annotation definitions and one or more pluggable modules that are used with the compiler for annotation processing.

This article begins with a brief overview of annotations, and then you’ll learn how to apply annotations to Java types, write type annotations, and use compile-time plug-ins for type checking. After reading this article, you’ll be able to use type annotations to enforce stronger typing in your Java source code.

Overview of Built-in Annotations

Annotations can be easily recognized in code because the annotation name is prefaced with the @ character. Annotations have no direct effect on code operation, but at processing time, they can cause an annotation processor to generate files or provide informational messages. 

In its simplest form, an annotation can be placed in Java source code to indicate that the compiler must perform specific “checking” on the annotated component to ensure that the code conforms to specified rules.

Java comes with a basic set of built-in annotations. The following Java annotations are available for use out of the box: 

NEED INFO? Annotations have no direct effect on code operation, but at processing time, they can cause an annotation processor to generate files or provide informational messages.

  • @Deprecated: Indicates that the marked element should no longer be used. Most often, another element has been created that encapsulates the marked element’s functionality, and the marked element will no longer be supported. This annotation will cause the compiler to generate a warning when the marked element is found in source code.
  • @Override: Indicates that the marked method overrides another method that is defined in a superclass. The compiler will generate a warning if the marked method does not override a method in a superclass.
  • @SuppressWarnings: Indicates that if the marked element generates warnings, the compiler should suppress those warnings.
  • @SafeVarargs: Indicates that the marked element does not perform potentially unsafe operations via its varargs parameter. Causes the compiler to suppress unchecked warnings related to varargs.
  • @FunctionalInterface: Indicates that the type declaration is intended to be a functional interface. 

Listing 1 shows a couple of examples using these built-in annotations.

@Override

public void start(Stage primaryStage) {
    ...
}

@Deprecated
public void buttonAction(ActionEvent event){
    ...
}

Listing 1

Use Cases for Annotations on Types

Annotations can exist on any Java type declaration or expression to help enforce stronger typing. The following use cases explain where type annotations can be of great value.

Generation of new objects. Type annotations can provide static verification when creating new objects to help enforce the compatibility of annotations on the object constructor. For example:

Forecast currentForecast = new @Interned Forecast();

Generics and arrays. Generics and arrays are great candidates for type annotations, because they can help restrict the data that is to be expected for these objects. Not only can a type annotation use compiler checking to ensure that the correct datatypes are being stored in these elements, but the annotations can also be useful as a visual reminder to the developer for signifying the intent of a variable or array, for example:

@NonEmpty Forecast []

Type casting. Type casts can be annotated to ensure that annotated types are retained in the casting. They can also be used as qualifiers to warn against unintended casting uses, for instance:

@Readonly Object x; …

 (@Readonly Date) x …

or

Object myObject =(@NotNull Object) obj

Inheritance. Enforcing the proper type or object that a class extends or implements can significantly reduce the number of errors in application code. Listing 2 contains an example of a type annotation on an implementation clause.

class MyForecast<T> implements @NonEmpty List< @ReadOnly T>

Listing 2

Exceptions. Exceptions can be an-no-tated to ensure that they adhere to certain criteria, for example:

catch (@Critical Exception e) {

 ... 
}

Receivers. It is possible to annotate a receiver parameter of a method by explicitly listing it within the parameter list. Listing 3 shows a demonstration of type annotations on a method receiver parameter.

class Weather {

    ...
    void tempCalc(@ReadOnly Weather this){}
    ...
}

Listing 3

Applying Type Annotations

Type annotations can be applied on types in a variety of ways. Most often, they are placed directly before the type to which they apply. However, in the case of arrays, they should be placed before the relevant part of the type. For instance, in the following declaration, the array should be read-only:

Forecast @Readonly [] fiveDay =new Forecast @Readonly [5];

When annotating arrays and array types, it is important to place the annotation in the correct position so that it applies to the intended element within the array. Here are a few examples: 

  • Annotating the int type: @ReadOnly int [] nums;
  • Annotating the array type int[]: int @ReadOnly [] nums;
  • Annotating the array type int[][]: int @ReadOnly [][] nums;
  • Annotating the type int[], which is a component type of int[][]: int [] @ReadOnly [] nums;

Using Available Type Annotations

To enforce stronger type checking, you must have a proper set of annotations that can be used to enforce certain criteria on your types. As such, there are a number of type-checking annotations that are available for use today, including those that are available with the Checker Framework.

Java SE 8 does not include any annotations specific to types, but libraries such as the Checker Framework contain annotations that can be applied to types for verifying certain criteria. For example, the Checker Framework contains the @NonNull annotation, which can be applied to a type so that upon compilation it is verified to not be null. The Checker Framework also contains the @Interned annotation, which indicates that a variable refers to the canonical representation of an object. The following are a few other examples of annotations available with the Checker Framework: 

  • @GuardedBy: Indicates a type whose value may be accessed only when the given lock is held
  • @Untainted: Indicates a type that includes only untainted, trusted values
  • @Tainted: Indicates a type that may include only tainted, untrusted values; a supertype of @Untainted
  • @Regex: Indicates a valid regular expression on Strings 

To make use of the annotations that are part of the Checker Framework, you must download the framework, and then add the annotation source files to your CLASSPATH, or—if you are using Maven—add the Checker Framework as a dependency.

If you are using an IDE, such as NetBeans, you can easily add the Checker Framework as a dependency using the Add Dependency dialog box. For example, Figure 1 shows how to add a dependency to a Maven project.

annotations-f1

Figure 1

Once the dependencies have been added to the application, you can begin to use the annotations on your types by importing them into your classes, as shown in Listing 4.


import checkers.interning.quals.Interned;
import checkers.nullness.quals.NonNull;
...
@ZipCode
@NonNull
String zipCode;

Listing 4

If you have a requirement that is not met by any of the existing implementations, you also have the option of creating custom annotations to suit your needs.

Defining Custom Annotations

Annotations are a form of interface, and an annotation type definition looks very similar to an interface definition. The difference between the two is that the annotation type definition includes the interface keyword prefixed with the @ character. Annotation definitions also include annotations themselves, which specify information about the type definition. The following list of annotations can be used when defining an annotation: 

  • @Retention: Specifies how the annotation should be stored. Options are CLASS (default value; not accessible at runtime), RUNTIME (available at runtime), and SOURCE (after the class is compiled, the annotation is disregarded).
  • @Documented: Marks the annotation for inclusion in Javadoc.
  • @Target: Specifies the contexts to which the annotation can be applied. Contains a single element, value, of type java.lang .annotation.ElementType[].
  • @Inherited: Marks the annotation to be inherited by subclasses of the annotated class. 

The definitions for standard declaration annotations and type annotation look very similar. The key differentiator is in the @Target specification, which denotes the kind of elements to which a particular annotation can be applied. Declaration annotations target fields, whereas type annotations target types.

A declaration annotation may contain the following meta-annotation:

@Target(ElementType.FIELD)

A type annotation must contain the following meta-annotation:

@Target(ElementType.TYPE_USE)

If the type annotation is to target a type parameter, the annotation must contain the following meta-annotation:

@Target (ElementType.TYPE_PARAMETER)

A type annotation may apply to more than one context. In such cases, more than one ElementType can be specified within a list. If the same ElementType is specified more than once within the list, then a compile-time error will be displayed.

Listing 5 shows the complete listing for an @NonNull type annotation definition. In the listing, the definition of @NonNull includes two targets, which means that the annotation may be applied to either a type or a type parameter.


@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.TYPE_USE, ElementType.TYPE_PARAMETER})
@TypeQualifier
public @interface NonNull {
}

Listing 5

Similar to standard declaration annotations, type annotations can also contain parameters, and they may contain default values. To specify parameters for type annotations, add their declarations within the annotation interface. Listing 6 demonstrates a type annotation with one parameter: a string field identified by zip.


@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE_USE)
@TypeQualifier
public @interface ZipCode {
    String zip() default "60605";
}

Listing 6

It is possible that an annotation can be applied on both a field and a type simultaneously. For instance, if an annotation of @Foo was applied to a variable declaration, it could also be applied to the type declaration at the same time if the @Target contains both elements. Such a scenario might look like the following declaration, which applies to both the type String and the variable myString

@Foo String myString; 

Processing Type Annotations

After you apply type annotations to the code, you must use a compiler plug-in to process the annotations accordingly. As mentioned previously, annotations have no operational effect on application code. The processor performs the magic as it parses the code and performs certain tasks when annotations are encountered.

If you write custom annotations, you must also write custom compiler plug-ins to process them. JSR 269, Pluggable Annotation Processing API, provides support for developing custom annotation processors. Developing an annotation processor is out of scope for this article, but the Pluggable Annotation Processing API makes it easy to do. There are also annotation processors available for download, such as the Checker Framework processors.

Using a type-qualifier compiler plug-in. The Checker Framework is a library that can be used within your applications, even if you are using older releases of Java. The framework contains a number of type annotations that are ready to be utilized, along with annotation processors that can be specified when compiling your code. Annotation support in Java SE 8 enables the use of third-party libraries such as the Checker Framework, making it easy to incorporate prebuilt type annotations into new and existing code.

Previously, we saw how to incorporate the Checker Framework into a project in order to make use of the type annotations that come with it. However, if you do not also use a custom annotation processor, these annotations will not be processed, and they’ll be useful only for documentation purposes.

The Checker Framework contains a number of custom processors for each of the type annotations that are available with the framework. Once the framework is installed onto a machine, a custom javac compiler that is packaged with the framework can be used to compile annotated applications.

To make use of the Checker Framework, simply download the .zip file and extract it to your machine. Optionally, update your execution path or create an alias to make it easy to execute the Checker Framework binaries. Once installed, the framework can be verified by executing a command similar to the one shown in Listing 7.

java -jar binary/checkers.jar -versionjavac 1.8.0-jsr308-1.7.1

Listing 7

It helps to know which annotations are available for use, so users of the Checker Framework should first review the framework documentation to read about them. The Checker Framework refers to type annotations as qualifiers. Once you are familiar with the qualifiers that are available for use, determine which of them might be useful to incorporate on types within existing applications. If you are authoring a new application, apply the qualifiers to types accordingly to take advantage of the benefits.

To check your code, the compiler must be directed as to which processors to use for type checking. This can be done by executing the custom javac normally and specifying the –processor flag along with the fully qualified processor name.

For instance, if the @NonNull annotation is used, then the nullness processor must be specified when compiling the code. If you have installed the Checker Framework, use the custom javac that is distributed with the framework, and specify the checkers.nullness.NullnessChecker processor to process the annotations.

Listing 8 contains a sample class that makes use of the @NonNull annotation. To compile this class, use the command in Listing 9.

If the class shown in Listing 8 is compiled, then no warnings will be noted.


import checkers.nullness.quals.*;
     public class WeatherTracker {
     
         public WeatherTracker(){}
         
         void obtainForecast() {
             @NonNull Object ref;
         }
}

Listing 8

javac -processor checkers.nullness.NullnessChecker WeatherTracker.java

Listing 9

However, assigning null to the annotated variable declaration will cause the nullness checker to provide a warning. Listing 10 shows the modified class, and Listing 11 shows the warning that the compiler will produce.


import checkers.nullness.quals.*;
     public class WeatherTracker {
     
         public WeatherTracker(){}
         
         void obtainForecast() {
             @NonNull Object ref = null;
         }
}

Listing 10


javac -processor checkers.nullness.NullnessChecker 
WeatherTracker.java
WeatherTracker.java:7: error: incompatible types in assignment.
             @NonNull Object ref = null;
                                                             ^
  found   : null
  required: @UnknownInitialization @NonNull Object
1 error

Listing 11

Rather than using the custom javac binary, you can use the standard JDK installation and run checkers.jar, which will utilize the Checker compiler rather than the standard compiler. Listing 12 demonstrates an invocation of checkers .jar, rather than the custom javac.


java -jar binary/checkers.jar
 -processor checkers.nullness.NullnessChecker WeatherTracker.java
WeatherTracker.java:7: error: incompatible types in assignment.
             @NonNull Object ref = null;
                                                             ^
  found   : null
  required: @UnknownInitialization @NonNull Object
1 error

Listing 12

The Checker Framework contains instructions for adding the custom javac command to your CLASSPATH and creating an alias, and it describes more ways to make it easy to integrate the framework into your compile process. For complete details, please see the documentation.

Compilation using multiple processors at once. What if you wish to specify more than one processor at compilation time? Via auto- discovery, it is possible to use multiple processors when compiling with the Checker Framework. To enable auto-discovery, a configuration file named META-INF/services/javax.annotation.processing.Processor must be placed within the CLASSPATH. This file must contain the name of each Checker plug-in that will be used, one per line. When using auto-discovery, the javac compiler will always run the listed Checker plug-ins, even if the –processor flag is not specified.

To disable auto-discovery, pass the –proc:none command-line option to javac. This option disables all annotation processing.

Using a Maven plug-in. A plug-in is available that allows the Checker Framework to become part of any Maven project. To use the plug-in, modify the project object model (POM) to specify the Checker Framework repositories, add the dependencies to your project, and attach the plug-in to the project build lifecycle. Listing 13, Listing 14, and Listing 15 show an example for accomplishing each of these tasks.


<!-- Add repositories to POM -->
<repositories>
  <repository>
    <id>checker-framework-repo</id>
    <url>http://types.cs.washington.edu/m2-repo</url>
  </repository>
</repositories>
<pluginRepositories>
  <pluginRepository>
    <id>checker-framework-repo</id>
    <url>http://types.cs.washington.edu/m2-repo</url>
  </pluginRepository>
</pluginRepositories>

Listing 13


<!-- Declare the dependency within the POM -->
<dependencies>
  ...
    <dependency>
      <groupId>edu.washington.cs.types.checker</groupId>
      <artifactId>checker-framework</artifactId>
      <version>1.7.0</version>
    </dependency>
   ...
</dependencies>

Listing 14 


<!-- Plugin to be specified in POM-->
<plugin>
  <groupId>types.checkers</groupId>
  <artifactId>checkers-maven-plugin</artifactId>
  <version>1.7.0</version>
  <executions>
    <execution>
    <!-- run the checkers after compilation;
     this can also be any later phase -->
      <phase>process-classes</phase>
      <goals>
        <goal>check</goal>
      </goals>
    </execution>
  </executions>
  <configuration>
    <!-- required configuration options -->
    <!-- a list of processors to run -->
    <processors>
      <processor>checkers.nullness.NullnessChecker</processor>
      <processor>checkers.interning.InterningChecker</processor>
    </processors>
    <!-- optional configuration options go here -->
  </configuration>
</plugin>

Listing 15

The benefits of type annotations and example use cases

Annotations were introduced into Java in Java SE 5, providing a form of syntactic metadata that can be added to program constructs. Annotations can be processed at compile time, and they have no direct effect on the operation of the code. However, they have many use cases. For instance, they can produce informational messages for the developer at compile time, detecting errors or suppressing warnings. In addition, annotations can be processed to generate Java source files or resources that can be used to modify annotated code. The latter is useful for helping to cut down on the amount of configuration resources that must be maintained by an application.

Using the Maven plug-in makes it easy to bind the Checker Framework to a project for use within an IDE as well. For instance, if the plug-in is configured on a NetBeans Maven project, the Checker Framework will process annotations each time the project is built within NetBeans.

Distributing Code Containing Type Annotations

To make use of a particular type annotation, its declaration must be within the CLASSPATH. The same holds true when distributing applications that contain type annotations. To compile or run source code containing type annotations, minimally the annotation declaration classes must exist within the CLASSPATH.

If you’ve written custom annotations, they might already be part of the application source code. If not, a JAR file containing those annotation declarations should be packaged with the code distribution. The Checker Framework includes a JAR file, checkers-quals.jar, which includes the declarations of the distributed qualifiers (annotations). If you are using the Checker Framework annotations within an application, you should package this JAR file with the distribution.

Conclusion

Java SE 8 adds support for type annotations. Type annotations can provide a stronger type-checking system, reducing the number of errors and bugs within code. Applications using type annotations are also backward compatible, because annotations do not affect runtime operation.

Developers can opt to create custom type annotations, or use annotations from third-party solutions. One of the most well-known type-checking frameworks is the Checker Framework, which can be used with Java SE 8 or previous releases. To begin making your applications less error-prone, take a look at the Checker Framework documentation.

Josh Juneau is an application developer, system analyst, and DBA. He primarily develops using Java, PL/SQL, and Jython/Python. He manages the Jython Monthly newsletter, Jython Podcast, and the Jython website. He authored Java EE 7 Recipes: A Problem-Solution Approach (Apress, 2013)and Introducing Java EE 7: A Look at What’s New (Apress, 2013).