Using null type annotations

Starting with Java 8, null annotations can be used in a new and more powerful way, because the new concept of "type annotations" (JSR 308) supports the use of annotations as an extension to the type system.

Technically, this is determined by two new elements in the enum java.lang.annotation.ElementType: TYPE_USE and TYPE_PARAMETER. Notably, when saying @Target(ElementType.TYPE_USE) the annotation thus marked can be attached basically to all usages of a type.

By interpreting null annotations as part of the type system we interpret each class or interface Cn in the system as introducing two distinct types: "@NonNull Cn" and "@Nullable Cn". The former type contains all instances of Cn whereas the latter type additionally contains the value null. This implies that @NonNull Cn is a subtype of @Nullable Cn with all regular consequences regarding assignability. So ideally for every value in a program we will know if it can be null (and must be checked before dereference) or not. The un-annotated type will be considered a legacy type just like raw types are legacy types since the introduction of generics: a way for interfacing with old code, to be flagged with warnings about unchecked conversions. If we systematically avoid such legacy types, then the compiler can rigorously flag every unsafe usage.

In order to achieve completeness of null analysis, checks regarding null type annotations have been integrated with all type checking tasks of the compiler (active if null annotations are enabled).

Users migrating from null annotations in previous versions to Java-8-style null type annotations are advised to check the section about compatibility.

Note, that the actual qualified names of null type annotations are configurable, but by default the ones shown here are used (from the package org.eclipse.jdt.annotation). When using 3rd party null annotation types, please ensure that those are properly defined using at least a @Target meta annotation, because otherwise the compiler can not distinguish between declaration annotations (Java 5) and type annotations (Java 8). Furthermore, some details of @NonNullByDefault are not supported when using 3rd party annotation types.

Generics

Perhaps the main advantage of type annotations for null analysis is the ability to annotate the parameters and arguments of generic classes and interfaces. Programmers only using generic classes may directly skip to the section on type arguments but designers of generic classes should take the time to understand the different implications of annotating these elements:

Type parameters

A generic class, interface or method may declare one or more type parameters. Technically these are declarations, and hence it was a mere oversight that these cannot be annotated in Java 5. In Java 8 an annotation can declare @Target(ElementType.TYPE_PARAMETER) to be applicable in this position. JDT's null type annotations @NonNull and @Nullable are declared with @Target({ TYPE_USE }), which includes usage on type parameter declarations.

With respect to null type annotations, each type parameter can be specified at one of these levels:

unconstrained
the type parameter does not impose any nullness-constraints on the arguments that a client my substitute for the type parameter.
constrained by an upper bound
the type parameter has an extends clause that specifies minimal nullness-requirements on type arguments provided by clients
exactly specified
the type parameter restricts usage to types of exactly one particular nullness

Constraining a type parameter via an upper bound relies on the fact that each type '@NonNull Cn' is a subtype of the corresponding type '@Nullable Cn'. Hence, a @Nullable upper bound does not impose any restriction, whereas a @NonNull upper bound prohibits the substitution by a @Nullable type argument:

    // declarations:
    class C0<T0 extends @Nullable Object> {} // meaningless, no constraint imposed
    class C1<T1 extends @NonNull Object> {}
    ...
    // usage:
    C1<@NonNull String> c1String;  // legal
    C1<@Nullable String> c1String; // illegal

For exact specification a null annotation may be attached to the type parameter declaration itself, which is interpreted as defining both an upper and a lower bound. In other words, only types with the exact same null type annotation are legal as type arguments:

    // declaration:
    class C2<@Nullable T2> {}
    ...
    // usage:
    C2<@NonNull String> c2String;  // illegal
    C2<@Nullable String> c2String; // legal

Given the asymmetry, that in Java a type parameter may declare only upper bounds but no lower bounds, the following three styles can be recommended:

Type variables

Within the scope of a generic declaration (class, interface or method), the name of a type parameter can be used as a type variable, i.e., a placeholder for a type that is not known at this point.

A type variable will typically be used without (further) null annotations, which implies that the annotations from the type parameter declaration will apply as detailed below. In some situations, however, it is useful to annotate an individual use of a type variable. As an example consider the library method java.util.Map.get(Object), which should actually be annotated like this:

    @Nullable V get(Object key)

By this declaration we would indicate that the return type is the nullable variant of whatever type V may represent. In other words, a null annotation on the use of a type variable overrides any other null information that would otherwise apply to this type. In particular any null annotation on the corresponding type parameter declaration (or its bound) is overridden by a null annotation in this position.

On the other hand, when using a type variable without immediate null annotations the following rules apply depending on the declaration of the corresponding type parameter:

A type variable corresponding to a type parameter with a @NonNull upper bound denotes a type that is known to be nonnull.

    class C1<T1 extends @NonNull Number> {
        int consume(T1 t) {
            return t.intValue(); // OK since T1 is known to be nonnull
        }
        T1 provide() {
            return null;         // NOT OK since T1 requires nonnull
        }
    }

A type variable corresponding to a type parameter specified as @Nullable denotes a type that is known to be nullable.

    class C2<@Nullable T2 extends Number> {
        int consume(T2 t) {
            return t.intValue(); // NOT OK since T2 is known to be nullable
        }
        T2 provide() {
            return null;         // OK: returning null is legal
        }
    }

A type variable corresponding to an unconstrained type parameter requires pessimistic checking in order to guarantee safety with all legal substitutions: this type can neither be assumed to be nullable nor nonnull.

    class C<T extends Number> {
        int consume(T t) {
            return t.intValue(); // NOT OK since T could be nullable
        }
        T provide() {
            return null;         // NOT OK since T could require nonnull
        }
    }

The last point may look surprising at first, but please see that an unconstrained type parameter implies that we may not assume anything about the nullness of the type represented by the corresponding type variable. Even more: we must actively support nullable and nonnull types. On the other hand this simply extends the existing rule that the only type being compatible with an unbounded type variable is the type variable itself. To explain this situation in the context of null analysis, the compiler will raise the following error against the return in provide():

Null type mismatch (type annotations): 'null' is not compatible to the free type variable 'T'

The severity of problems detected by this pessimistic analysis is controlled by a dedicated preference option.

By enforcing this defensive strategy regarding unconstrained type parameters we obtain the benefit of allowing clients to freely choose the rules for a particular generic instantiation, as will be shown next.

Type arguments

When instantiating a generic type or when invoking a generic method, the constraints put forward by the type parameter must be observed. Hence, when a provider of a generic type or method specified the required nullness, this must be obeyed and the compiler will flag any violations.

When, on the other hand, a type parameter does not impose any restrictions, a client may freely choose the nullness of his type arguments:

    int processWithoutNulls (@NonNull List<@NonNull Integer> ints) {
        int result = 0;
        for (int i = 0; i < ints.size(); i++) {
            Integer element = ints.get(i);
            result += element.intValue(); // OK: list element is known to be nonnull
            ints.set(i, null);            // NOT OK: list does not accept null value
        }
        return result;
    }
    int processWithNulls (@NonNull List<@Nullable Integer> ints) {
        int result = 0;
        for (int i = 0; i < ints.size(); i++) {
            Integer element = ints.get(i);
            result += element.intValue(); // NOT OK: list element can be null
            ints.set(i, null);            // OK: list accepts null value
        }
        return result;
    }

Substitution

The intention behind combining null type annotations with generics is to propagate a constraint defined for a type argument into all occurrences of the corresponding type variable. For example, if you declare a variable of type List<@NonNull String> and invoke any method from List<T> on this variable, all method signatures will see type T substituted by @NonNull String. This is how inserting a null value into this list is made impossible, and allows to safely regard elements extracted from this list as nonnull. The previous section gave examples of exactly this idea.

Unfortunately, this idea introduces a new risk when applied to generic library classes that are not designed with null annotations in mind. A prominent example is method java.util.Map.get(K), which declares to return V. In this particular case, the javadoc of said method explicitly states that null is a possible return value, which is in conflict with substituting V by any nonnull type. So, if this specific method get() is invoked on a variable of type Map<Y,@NonNull X>, it is unsafe to assume that the return value is nonnull. This dilemma is a combination of two factors:

  1. The library lacks null annotations (it should be considered as "legacy" in terms of null annotations)
  2. The compiler cannot know whether an unannotated type variable is by intention (as to support arbitrary substitution) or an unsafe omission (legacy).

To alert users about this risk, a specific warning is raised by the compiler:

Unsafe interpretation of method return type as '@NonNull X' based on the receiver type 'Map<Y,@NonNull X>'. Type 'Map' doesn't seem to be designed with null type annotations in mind

The severity of this problem is controlled by a dedicated preference option.

In response to this warning, the resolution of the dilemma is to add null annotations to the generic class in question. For the likely case that the current user is not the owner of the legacy library, external null annotations should be used. Then there are two options:

  1. For the given example, method get(K) should be declared to return @Nullable V.
  2. For the opposite case as exemplified by List.get(), the return type should be left unannotated. In order to signal to the compiler that types are left unannotated by intention, a stub external annotation file (.eea) should be created without inserting actual external annotations. This will tell the compiler that this class is no longer to be considered as legacy, and hence all signatures of this class should be interpreted verbatim according to the rules given above (care must be taken that this is safe for all method in that class).

If an external annotation file is found, the specific warning about unsafe interpretation is not issued. Finally, if a project is not yet configured for using external annotations for the given library, the problem is softened to "info" severity.

Inference

With null type annotations affecting type arguments, the language features one more location amenable to inference: during type inference for the invocation of a generic method (lambda expression etc.), type inference shyly attempts to also infer the appropriate null type annotations. Example:

    <T> T check(T in) { return in; }
    void test(@NonNull List<@Nullable String> someStrings) {
        @NonNull List<@Nullable String> checked;
        checked = check(someStrings); // inferring types for this invocation
        ...
    }

In this trivial example, inference will indeed instantiate the generic parameter <T> to @NonNull List<@Nullable String>. More complex scenarios are inferred, too, but no guarantee is made, that a possible solution will always be found. In case inference fails to infer suitable null type annotations, users are advised to revert to explicitly specify type arguments even of a generic method invocation.

More locations

Cast and instanceof

Syntactically, type annotations can be used also in casts and instanceof expressions. For null annotations, however, this has limited value.

Casting to a null-annotated type is always an unchecked cast because the compiler is not allowed to insert runtime checks that would make the cast meaningful. If a runtime check is desired, please consider using a small helper function like:

    static @NonNull <T> T castToNonNull(@Nullable T value, @Nullable String msg) {
        if (value == null) throw new NullPointerException(msg);
        return value;
    }

Casts affecting the type arguments of a generic type will always be unchecked casts due to erasure.

instanceof checks with null type annotations are not meaningful. Hence the compiler flags this as illegal usage of a null type annotation.

Locations that are nonnull by definition

Syntactically, type annotations can also be used for

In each of these constructs, the type is nonnull by definition. Hence a null type annotation in one of these positions is flagged as illegal use. This doesn't, however, restrict the use of null type annotations on type arguments of the given type.

Compatibility

Migrating from declaration annotations to type annotations has a few unavoidable implications, regarding the syntax, regarding project configuration and regarding the semantics.

Syntax

For two constructs the JLS introduces a syntactic change:

Declaration Annotations (Java 7 or below)Type Annotation (Java 8)
@NonNull String[]String @NonNull[]
@NonNull java.lang.Stringjava.lang.@NonNull String

In both cases the new syntax has been introduced to provide more options.

For arrays a type annotation before the leaf element type will now denote an array whose individual cells have the given nullness - here: cells cannot be null. In Java 7 and below the same syntax expressed a property of the corresponding variable and hence captured the nullness of the array itself. To express the same using Java-8 type annotations, viz. that the array itself can or cannot be null, the type annotation is placed before the square brackets denoting the array dimensions. This implies that the old syntax is still valid, but its meaning has changed:

    // annotated leaf type:
    @NonNull Object [] o1;
    o1 = null;          // OK
    o1 = new Object[1];
    o1[0] = null;       // NOT OK
    ...
    // annotated array type:
    Object @NonNull[] o2;
    o2 = null;          // NOT OK
    o2 = new Object[1];
    o2[0] = null;       // OK
    ...
    // multi-dimensional array:
    Object @NonNull[] @Nullable[] o3;
    o3 = null;          // NOT OK, outer array is nonnull
    o3 = new Object[1] @Nullable[];
    o3[0] = null;       // OK, inner array is nullable

Unfortunately, checking proper initialization of an array with nonnull content is beyond the capabilities of JDT's static analysis.

For qualified type names the type annotation must be placed directly preceding the actual type name. This way it is possible to give different type annotations for inner classes and their enclosing like in org.project.@Immutable Outer.@Nullable Inner. This distinction, however, is not useful for null annotations, because the enclosing of a non-static inner class is by definition always non-null. Users of null type annotations only need to understand that the old syntax for this case is illegal for type annotations and how to convert this into legal Java 8 code (see the table above).

Project configuration

Properly designed annotation types can be distinguished by looking at their @Target declaration (the use of null annotations lacking a @Target declaration is discouraged). To support both styles of annotations, JDT has published a major update of the annotation bundle org.eclipse.jdt.annotation: Versions 1.1.x are old style declaration annotations; versions 2.0.0 and onward are type annotations. By increasing the major version an incompatibility is signaled. Users are advised to reference this library with an explicit version range, either [1.1.0,2.0.0) for declaration annotations or [2.0.0,3.0.0) for type annotations.

The exact configuration depends of course on the flavor of project:

Plain Java
JDT continues to offer a quickfix for copying the annotation library into the project. The version will be determined by the compliance settings of the project.
Maven
Both versions of the annotation bundle will be published to repo.eclipse.org, from where they can be consumed using the regular maven mechanisms: be sure to specify the correct version; specifying <scope>compile</scope> is recommended for this dependency.
OSGi / Eclipse
When developing OSGi bundles / Eclipse plugins the version range should be specified as mentioned above. Unfortunately, OSGi doesn't support a concept of compile time dependencies. The PDE specific mechanism in file build.properties is problematic because it doesn't support specifying a version range. Thus the best approximation of the desired semantics is to use a Require-Bundle dependency. qualified with resolution:=optional in order to avoid forcing this dependency on the runtime:
Require-Bundle: ...,
 org.eclipse.jdt.annotation;bundle-version="[2.0.0,3.0.0)";resolution:=optional

Semantics – NonNullByDefault

While the fundamental semantics of null annotation remains unchanged, the annotation @NonNullByDefault has been changed slightly:

Note, that the value property described here is only supported when using the annotation type org.eclipse.jdt.annotation.NonNullByDefault supplied by Eclipse.

Although the Java 8 variant of @NonNullByDefault affects more locations, two noteworthy exceptions exist (as specified in DefaultLocation):

Wildcards and the use of type variables are always excluded from @NonNullByDefault.

By this rule, type variables and wildcards retain their intended properties as "unknowns" also in terms of nullness, even when they appear in the context of @NonNullByDefault.

Compiler messages explained

In addition to compiler messages of the previous version the following messages may be issued, if null type annotations are enabled for analysis:

General null type mismatch

Null type mismatch (type annotations): required 'X' but this expression has type 'Y'
In an assignment context null type annotations don't match. Note that the mismatch may relate to any detail of the type (type argument, array element), not necessarily to the main type.

All mismatches detected based on type annotations are prefixed with "Null type mismatch (type annotations)".

Various expressions

Potential null pointer access: array element may be null
An array element is dereferenced, where the array type declares its elements as @Nullable.
Potential null pointer access: this expression has a '@Nullable' type
Any expression at the left hand side of a dot has a type that is declared to be nullable.
Redundant null check: comparing '@NonNull X' against null
An arbitrary expression known to have a @NonNull type is unnecessarily being compared against null.

Unchecked conversions

Null type safety (type annotations): The expression of type 'X' needs unchecked conversion to conform to '@NonNull X'
A value of an un-annotated type is being assigned to a variable of an annotated type. Note that the mismatch may relate to any detail of the type (type argument, array element), not necessarily to the main type.
Null type safety: Unchecked cast from X to '@N Y'
A value is casted to a null-annotated type, where the nullness is not checked at runtime by the cast.

Problems specific to generics

Null constraint mismatch: The type 'X' is not a valid substitute for the type parameter 'T'
Here the type parameter <T> has a constraint in one of the forms mentioned above. The actual type argument X, however, doesn't conform to this constraint.
This nullness annotation conflicts with a '@N' annotation which is effective on the same type parameter
A null annotation on a bound of a type parameter conflicts with another null annotation on another bound or on the type parameter itself.
Contradictory null annotations: method was inferred as 'T foo(X)', but only one of '@NonNull' and '@Nullable' can be effective at any location
Type inference for a generic method invocation has produced a signature in which contradictory null annotations clash on the same element.
Null type mismatch (type annotations): 'null' is not compatible to the free type variable 'T'
Null type mismatch (type annotations): required 'T' but this expression has type '@Nullable T', where 'T' is a free type variable
Null type safety: required '@NonNull' but this expression has type 'T', a free type variable that may represent a '@Nullable' type
The field 'f' may not have been initialized, whereas its type 'T' is a free type variable that may represent a '@NonNull' type
Potential null pointer access: this expression has type 'T', a free type variable that may represent a '@Nullable' type
These problems are specifically detected by pessimistic analysis for free type variables.
Unsafe interpretation of method return type as '@NonNull X' based on the receiver type 'Map<Y,@NonNull X>'. Type 'Map' doesn't seem to be designed with null type annotations in mind
Unsafe interpretation of method return type as '@NonNull X' based on substitution 'V=@NonNull X'. Declaring type 'Map<K,V>' doesn't seem to be designed with null type annotations in mind
This signals a particular dilemma regarding substitution of type variables from a legacy library.

Lambda expressions and method references

For any mismatches in null annotations affecting lambda expressions or method references the corresponding "descriptor" is mentioned (the single abstract method being implemented by the lambda / method reference). This is useful for finding the origin of a null annotation that is not explicit at the current expression.