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:

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:

Use an unconstrained type parameter to support type arguments of either nullness.

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.

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.

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():

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:

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:

The library lacks null annotations (it should be considered as "legacy" in terms of null annotations)

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

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:

For the given example, method get(K) should be declared to return @Nullable V.

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:

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:

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

allocation expressions

method receiver (pseudo argument by the name of this)

catch parameter

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

java.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:

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:

Unchecked conversions

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.

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.