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.
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:
extends
clause that specifies
minimal nullness-requirements on type arguments provided by clients
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:
@NonNull
upper bound for constraining type arguments to nonnull types.@Nullable
for constraining type arguments to nullable types.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.
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; }
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:
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:
get(K)
should be declared to return @Nullable V
.
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.
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.
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.
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.
Migrating from declaration annotations to type annotations has a few unavoidable implications, regarding the syntax, regarding project configuration and regarding the semantics.
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:
// 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).
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:
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.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
While the fundamental semantics of null annotation remains unchanged,
the annotation @NonNullByDefault
has been changed slightly:
value
property of the annotation
(see also the enum DefaultLocation
).Declaration Annotations (Java 7 or below) | Type Annotation (Java 8) |
---|---|
@NonNullByDefault(false) | @NonNullByDefault({}) |
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
.
In addition to compiler messages of the previous version the following messages may be issued, if null type annotations are enabled for analysis:
All mismatches detected based on type annotations are prefixed with "Null type mismatch (type annotations)
".
@Nullable
.<T>
has a constraint in one of the forms mentioned above.
The actual type argument X
, however, doesn't conform to this constraint.