Put a note on it

JSR 308 Explained: Java Type Annotations

JoshJuneau
labels

Josh Juneau presents the case for type annotations, illustrated with some examples of where they come to the fore.

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.

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. 

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.

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: 

  • @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 theChecker
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 @Targetspecification, 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 -version
javac 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
thecheckers.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
customjavac.

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

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 theCLASSPATH. 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.

Originally published in the March/April 2014 issue
of Java MagazineSubscribe today.

About the author

juneau-headshot

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).

 

 

 

 

(1) Originally published in the March/April 2014 Edition of
Java Magazine 
(2) Copyright © [2014] Oracle.

Author
Comments
comments powered by Disqus