Nullness User Guide
In Java code, whether an expression may evaluate to null is often documented
only in natural language, if at all. JSpecify's nullness annotations let
programmers express the nullness of Java code in a consistent and well-defined
way.
JSpecify defines annotations that describe whether a Java type contains the
value null. Such annotations are useful to (for example):
- programmers reading the code,
- tools that help developers avoid
NullPointerExceptions, - tools that perform run-time checking and test generation, and
- documentation systems.
Java variables are references
In Java, all non-primitive variables are either null or a reference to an
object. We often think of a declaration like String x as meaning that x is a
String, but it really means that x is either null or a reference to an
actual String object. JSpecify gives you a way to make it clear whether you
really mean that, or you really mean that x is definitely a reference to a
String object and not null.
Types and nullness
JSpecify gives you rules that determine, for each type usage, which of four kinds of nullness it has:
- It can include
null(it is "nullable"). - It will not include
null(it is "non-nullable"). - For type variables only: it includes
nullif the type argument that is substituted for it does (it has "parametric nullness"). - We don't know whether it can include
null(it has "unspecified nullness"). This is equivalent to the state of the world without JSpecify annotations.
For a given variable x, if x can be null then x.getClass() is unsafe
because it could produce a NullPointerException. If x can't be null,
x.getClass() can never produce a NullPointerException. If we don't know
whether x can be null or not, we don't know whether x.getClass() is safe
(at least as far as JSpecify is concerned).
The notion of "can't be null" should really be read with a footnote that says
"if none of the code in question involves unspecified nullness". For example, if
you have some code that passes types with unspecified nulless to a method that
accepts only @NonNull arguments, then tools might allow it to pass a
possibly-null value to a method that is expecting a "can't be null"
parameter.
There are four JSpecify annotations that are used together to indicate the nullness of all type usages:
- two type use annotations that indicate whether a specific type usage
includes
nullor not:@Nullableand@NonNull - a scope annotation that lets you avoid typing
@NonNullmost of the time:@NullMarked - another scope annotation that undoes the effects of
@NullMarkedso you can adopt annotations incrementally:@NullUnmarked
@Nullable and @NonNull
When a type is annotated with @Nullable, it means that a value of the type
can be null. @Nullable String x means that x might be null. Code that
uses those values must be able to deal with the null case, and it's okay to
assign null to such variables or pass null to those parameters.
When a type is annotated with @NonNull, it means that no value of the type
should be null. @NonNull String x means that x should never be null.
Code that uses those values can assume they're not null, and it's a bad idea
to assign null to those values or pass null to those parameters. (See
below for how to avoid having to spell out @NonNull most of the
time.)
static @Nullable String emptyToNull(@NonNull String x) {
return x.isEmpty() ? null : x;
}
static @NonNull String nullToEmpty(@Nullable String x) {
return x == null ? "" : x;
}
In this example, the parameter to emptyToNull is annotated with @NonNull, so
it cannot be null; emptyToNull(null) is not a valid method call. The body of
the emptyToNull method relies on that assumption and immediately calls
x.isEmpty(), which would throw NullPointerException if x were actually
null. Conversely, emptyToNull may return null, so its return type is
annotated with @Nullable.
On the other hand, nullToEmpty promises to handle null arguments, so its
parameter is annotated with @Nullable to indicate that nullToEmpty(null) is
a valid method call. Its body considers the case where the argument is null
and won't throw NullPointerException. It cannot return null, so its return
type is annotated with @NonNull.
void doSomething() {
// OK: nullToEmpty accepts null but won't return it
int length1 = nullToEmpty(null).length();
// Not OK: emptyToNull doesn't accept null; also, it might return null!
int length2 = emptyToNull(null).length();
}
Tools can use the @Nullable and @NonNull annotations to warn users about
calls that are unsafe.
As far as JSpecify is concerned, @NonNull String and @Nullable String are
different types. A variable of type @NonNull String can reference any
String object. A variable of type @Nullable String can too, but it can also
be null. This means that @NonNull String is a subtype of @Nullable String, in the same way that Integer is a subtype of Number. One way to
look at this is that a subtype narrows the range of possible values. A Number
variable can be assigned from an Integer but it can also be assigned from a
Long. Meanwhile an Integer variable can't be assigned from a Number (since
that Number might be a Long or some other subtype). Likewise, a @Nullable String can be assigned from a @NonNull String but a @NonNull String can't
be assigned from a @Nullable String (since that might be null).
class Example {
void useNullable(@Nullable String x) {...}
void useNonNull(@NonNull String x) {...}
void example(@Nullable String nullable, @NonNull String nonNull) {
useNullable(nonNull); // JSpecify allows this
useNonNull(nullable); // JSpecify doesn't allow this
}
}
What about unannotated types?
A type like String that isn't annotated with either @Nullable or @NonNull
means what it always used to mean: its values might be intended to include
null or might not, depending on whatever documentation you can find (but see
below for help!). JSpecify calls this "unspecified nullness".
class Unannotated {
void whoKnows(String x) {...}
void example(@Nullable String nullable) {
whoKnows(nullable); // ¯\_(ツ)_/¯
}
}
@NullMarked
It would be annoying to have to annotate each and every type usage in your Java
code with either @Nullable or @NonNull to avoid unspecified nullness
(especially once you add generics!).
So JSpecify gives you the @NullMarked annotation. When you apply
@NullMarked to a module, package, class, or method, it means that unannotated
types in that scope are treated as if they were annotated with @NonNull.
(Below we will see that there are some exceptions to this for
local variables and type variables.)
In code covered by @NullMarked, String x means the same as @NonNull String x.
If applied to a module then its scope is all the code in the module. If applied
to a package then its scope is all the code in the package. (Note that packages
are not hierarchical; applying @NullMarked to package com.foo does not
make package com.foo.bar @NullMarked.) If applied to a class, interface, or
method, then its scope is all the code in that class, interface, or method.
@NullMarked
class Strings {
static @Nullable String emptyToNull(String x) {
return x.isEmpty() ? null : x;
}
static String nullToEmpty(@Nullable String x) {
return x == null ? "" : x;
}
}
Here's the example from above, where the class containing the methods is
annotated with @NullMarked. The nullness of the types is the same as before:
emptyToNull does not accept null arguments, but it might return null;
nullToEmpty does accept null arguments, but it won't return null. But we
were able to do that with fewer annotations. In general, using @NullMarked
will give you correct nullness semantics with fewer annotations. In
@NullMarked code, you'll get used to thinking about plain, unannotated types
like String as meaning a real reference to a String object and never null.
As mentioned above, there are some exceptions to this interpretation for local variables and type variables.
@NullUnmarked
If you're applying JSpecify annotations to your code, you might not be able to
annnotate it all at once. If you can apply @NullMarked to some of your code
now, and do the rest later, that's better than waiting until you have time to
annotate everything. But that means you may have to null-mark a module, package
or class except for some classes or methods. To do that, apply
@NullUnmarked to a package, class, or method that's already inside a
@NullMarked context. @NullUnmarked simply undoes the effects of the
surrounding @NullMarked, so that unannotated types have unspecified nullness
unless they are annotated with @Nullable or @NonNull, as if there were no
enclosing @NullMarked at all. A @NullUnmarked scope may in turn contain a
nested @NullMarked element to make most unannotated type usages non-null
within that narrower scope.
Local variables
@Nullable and @NonNull aren't applied to local variables—at least not their
root types. (They should be applied to type arguments and array components.) The
reason is that it is possible to infer whether a variable can be null based
on the values that are assigned to the variable. For example:
@NullMarked
class MyClass {
void myMethod(@Nullable String one, String two) {
String anotherOne = one;
String anotherTwo = two;
String oneOrTwo = random() ? one : two;
String twoOrNull = Strings.emptyToNull(two);
...
}
}
Analysis can tell that all of these variables except anotherTwo can be null.
anotherTwo can't be null since two can't be null: it is not @Nullable
and it is inside the scope of @NullMarked. anotherOne can be null since it
is assigned from a @Nullable parameter. oneOrTwo can be null because it
may assigned from a @Nullable parameter. And twoOrNull can be null because
its value comes from a method that returns @Nullable String.
Generics
When you are using a generic type, the rules about @Nullable, @NonNull, and
@NullMarked are as you would expect from what we have seen. For example,
within a @NullMarked context, List<@Nullable String> means a reference to a
List (not null) where each element is either a reference to a String
object or null; but List<String> means a list (not null) where each
element is a reference to a String object and can't be null.
Declaring generic types
Things are a bit more complicated when you are declaring a generic type. Consider this:
@NullMarked
public class NumberList<E extends Number> implements List<E> {...}
The extends Number defines a bound for the type variable E. It means that
you can write NumberList<Integer>, since Integer can be assigned to
Number, but you can't write NumberList<String>, since String can't be
assigned to Number. This is standard Java behavior.
But now let's think about that bound as it relates to @NullMarked. Can we
write NumberList<@Nullable Integer>?
Within @NullMarked, remember, unannotated types are the same as if they were
annotated with @NonNull. Since the bound of E is the same as @NonNull Number, and not @Nullable Number, that means the type argument for E can't
be a type that includes null. @Nullable Integer can't be the type argument,
then, since that can include null. (In other words: @Nullable Integer is
not a subtype of Number.)
Inside @NullMarked, if you want to be able to substitute nullable type
arguments for a type parameter, you must explicitly provide a @Nullable bound
on your type variable:
@NullMarked
public class NumberList<E extends @Nullable Number> implements List<E> {...}
Now it is legal to write NumberList<@Nullable Integer>, since @Nullable Integer is assignable to the bound @Nullable Number. It's also legal to
write NumberList<Integer>, since plain Integer is assignable to @Nullable Number. Inside @NullMarked, plain Integer means the same thing as @NonNull Integer: a reference to an actual Integer value, never null. That just
means that the values represented by E can be null on some other
parameterization of NumberList, but not in an instance of
NumberList<Integer>.
Of course this assumes that List itself is written in a way that allows
nullable type arguments:
@NullMarked
public interface List<E extends @Nullable Object> {...}
If it were interface List<E> rather than interface List<E extends @Nullable Object> then NumberList<E extends @Nullable Number> implements List<E> would
not be legal. That's because interface List<E> is short for interface List<E extends Object>. Inside @NullMarked, plain Object means "Object reference
that can't be null". The <E extends @Nullable Number> from NumberList
would not be compatible with <E extends Object>.
The implication of all this is that every time you define a type variable like
E you need to decide whether it can be substituted with a @Nullable type. If
it can, then it must have a @Nullable bound. Often this will be <E extends @Nullable Object>. On the other hand, if it can't represent a @Nullable
type, that is expressed by not having @Nullable in its bound (including the
case of not having an explicit bound at all). Here's another example:
@NullMarked
public class ImmutableList<E> implements List<E> {...}
Here, because it is ImmutableList<E> and not ImmutableList<E extends @Nullable Object>, it is not legal to write ImmutableList<@Nullable String>.
You can only write ImmutableList<String>, which is a list of non-null String
references.
Using type variables in generic types
Let's look at what the methods in the List interface might look like:
@NullMarked
public interface List<E extends @Nullable Object> {
boolean add(E element);
E get(int index);
@Nullable E getFirst();
Optional<@NonNull E> maybeFirst();
...
}
The parameter type E of add means a reference that is compatible with the
actual type of the List elements. Just as you can't add an Integer to a
List<String>, you also can't add a @Nullable String to a List<String>, but
you can add it to a List<@Nullable String>.
Similarly, the return type E of get means that it returns a reference with
the actual type of the list elements. If the list is a List<@Nullable String>
then that reference is a @Nullable String. If the list is a List<String>
then the reference is a String.
On the other hand, the return type @Nullable E of the (fictitious) getFirst
method is always @Nullable. It will be @Nullable String whether called on a
List<@Nullable String> or a List<String>. The idea is that the method
returns the first element of the list, or null if the list is empty.
Similarly, the real methods @Nullable V get(Object key) in Map and
@Nullable E peek() in Queue can return null even when V and E can't be
null.
The distinction here is an important one that is worth repeating. A use of a
type variable like E should only be @Nullable E if it means a reference that
can be null even if E itself can't be null. Otherwise, plain E means a
reference that can only be null if E is a @Nullable type, like @Nullable String in this example. (And, as we've seen, E can only be a @Nullable type
if the definition of E has a @Nullable bound like <E extends @Nullable Object>.)
Similarly, you can use @NonNull E to indicate a type that is non-nullable
even when E is nullable. The fictitious maybeFirst() method returns a
non-nullable Optional. An Optional object can hold only non-null values, so
it's reasonable to define it as class Optional<T>; that is, its type argument
must not be nullable. So even for a List<@Nullable String>, maybeFirst() has
to return Optional<@NonNull String>. The way to declare that is to declare the
return type of maybeFirst() as Optional<@NonNull E>.
We saw earlier that @NullMarked usually means "references can't be null
unless they are marked @Nullable", and also that that doesn't apply to local
variables. Here we see that it doesn't apply to unannotated type variable uses
either, since an unannotated type variable usage whose bound is @Nullable may
be substituted with a @Nullable type argument.
Using type variables in generic methods
Essentially the same considerations that we just saw with generic types apply to generic methods too. Here's an example:
@NullMarked
public class Methods {
public static <T> @Nullable T
firstOrNull(List<T> list) {
return list.isEmpty() ? null : list.get(0);
}
public static <T> T
firstOrNonNullDefault(List<T> list, T defaultValue) {
return list.isEmpty() ? defaultValue : list.get(0);
}
public static <T extends @Nullable Object> T
firstOrDefault(List<T> list, T defaultValue) {
return list.isEmpty() ? defaultValue : list.get(0);
}
public static <T extends @Nullable Object> @Nullable T
firstOrNullableDefault(List<T> list, @Nullable T defaultValue) {
return list.isEmpty() ? defaultValue : list.get(0);
}
}
The firstOrNull method will accept a List<String> but not a List<@Nullable String>. When given an argument of type List<String>, T is String, so the
return type @Nullable T is @Nullable String. The input list can't contain
null elements, but the return value can be null.
The firstOrNonNullDefault method again does not allow T to be a @Nullable
type, so List<@Nullable String> is not allowed. Now the return value is not
@Nullable either which means it will never be null.
The firstOrDefault method will accept both List<String> and List<@Nullable String>. In the first case, T is String, so the type of the defaultValue
parameter and of the return value is String, meaning neither can be null. In
the second case, T is @Nullable String, so the type of defaultValue and of
the return value is @Nullable String, meaning either can be null.
The firstOrNullableDefault method again accepts both List<String> and
List<@Nullable String>, but now the defaultValue parameter is marked
@Nullable so it can be null even in the List<String> case. Likewise the
return value is @Nullable T so it can be null even when T can't.
Here's another example:
@NullMarked
public static <T> List<@Nullable T> nullOutMatches(List<T> list, T toRemove) {
List<@Nullable T> copy = new ArrayList<>(list);
for (int i = 0; i < copy.size(); i++) {
if (copy.get(i).equals(toRemove)) {
copy.set(i, null);
}
}
return copy;
}
This takes a List<T>, which by definition does not contain null elements,
and produces a List<@Nullable T>, with null in place of every element that
matched toRemove. The output is a List<@Nullable T> because it can contain
null elements, even if T itself can't be null.
Some subtler details
The previous sections cover 99% of everything you need to know to be able to use JSpecify annotations effectively. Here we'll cover a few details you probably won't need to know.
Type-use annotation syntax
There are a couple of places where the syntax of type-use annotations like
@Nullable and @NonNull may be surprising.
-
For a nested static type like
Map.Entry, if you want to say that the value can benullthen the syntax isMap.@Nullable Entry. You can often avoid dealing with this by importing the nested type directly, but in this caseimport java.util.Map.Entrymight be undesirable becauseEntryis such a common type name. -
For an array type, if you want to say that the elements of the array can be
nullthen the syntax is@Nullable String[]. If you want to say that the array itself can benullthen the syntax isString @Nullable []. And if both the elements and the array itself can benull, the syntax is@Nullable String @Nullable [].
A good way to remember this is that it is the thing right after @Nullable that
can be null. It is the Entry that can be null in Map.@Nullable Entry,
not the Map. It is the String that can be null in @Nullable String[] and
it is the [], meaning the array, that can be null in String @Nullable [].
Wildcard bounds
Inside @NullMarked, wildcard bounds work almost exactly the same as type
variable bounds. We saw that <E extends @Nullable Number> means that E can be
a @Nullable type and <E extends Number> means it can't. Likewise, List<? extends @Nullable Number> means a list where the elements can be null, and
List<? extends Number> means they can't.
However, there's a difference when there is no explicit bound. We saw that a
type variable definition like <E> means <E extends Object> and that means it
is not @Nullable. But <?> actually means <? extends B>, where B is the
bound of the corresponding type variable. So if we have
interface List<E extends @Nullable Object> {...}
then List<?> means the same as List<? extends @Nullable Object>. If we have
class ImmutableList<E> implements List<E> {...}
then we saw that that means the same as
class ImmutableList<E extends Object> implements List<E>
so ImmutableList<?> means the same as ImmutableList<? extends Object>. And
here, @NullMarked means that Object excludes null. The get(int) method
of List<?> can return null but the same method of ImmutableList<?> can't.