JSR 308 Explained: Java Type Annotations

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 itsvarargs
parameter. Causes the compiler to suppress unchecked warnings related tovarargs
.@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 ofint[][]
: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.
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), andSOURCE
(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 typejava.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 -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 Magazine. Subscribe today.
About the author
|
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.