2025-09-26
Sir Antony Hoare introduced the concept of Null-references or a Null pointer back in 1965 with the release of ALGOL W. In 2009 he spoke about it as his “Billion Dollar Mistake”. Dereferencing a Null pointer will address an invalid memory region, usually leading to runtime errors or complete crashes. Despite many efforts, this is still a thing in 2025.
In Java, there are no pointers in the sense of C. However, all
non-primitive variables are object references and these can be
null
. Accessing an uninitialized variable or a variable
with the value null
results in the infamous
NullPointerException
. As of 2025, there are only mere
wishes to have Null-restricted and nullable types in the Java language
itself, hence people are still looking for a solution they can have
now.
@NonNull
-standardsObviously, there’s a xkcd comic that says it all:
The first milestone release of JSpecify appeared in 2020. By then, it was preceded by
org.jetbrains.annotations.NotNull
: JetBrains specific
annotations for static code analysisorg.eclipse.jdt.annotation. NonNull
: Eclipse
compiler-specific annotations for static code analysis@NonNull
from Project Lombok,
not intended as a verification tool, but rather an instruction to add
null checks to the bytecodejavax.valiation.constraints
, more of a tool for
validating input valuesThe above list is not even complete, so why another standard, and what makes JSpecify special? First of all, JSpecify was defined and designed by a committee, similar to Java Specification Requests (JSR). JSRs are usually led by experts from industry and research. JSpecify is led by, among others
The range of companies involved demonstrates their shared interest and goal: to provide a new set of annotations with precise semantics regarding nullability and usage that several heavyweights in the industry were able to agree on.
We need to first understand the concept of “Nullness” to be able to
use JSpecify effective. 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. JSpecify calls this “unspecified nullness”. By using JSpecify
annotations, you can express that
null
null
null
or cannot be
null
(i.e. elements of a List<T>
)JSpecify consists of four annotations
@Nullable
and
@NonNull
@NullMarked
und
@NullUnmarked
The latter mark a package or a complete Java-module either as
null-safe, that is all parameters are assumed to @NonNull
,
so that each parameter that might be null, must be annotated with
@Nullable
. The usage of @NonNull
in that scope
is superfluous. @NullUnmarked
expresses the opposite and
reverses the effect of @NullMarked
. This is helpful to
migrate individual packages of a module step by step.
@Nullable
and @NonNull
aren’t applied to
local variables, 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 when everything else is null-checked.
Hierarchical, or not? Java-Packages and Modules
Java packages are not hierarchical. The two packages
a.b
anda.b.c
do not have a “c is part of b” relationship, even though many tools represent or evaluate it differently. Java modules, on the other hand, have a hierarchical relationship with packages: packages are subelements of a module. Therefore,@NullMarked
can be applied semantically correctly at the Java module level and@NullUnmarked
can be applied as needed at the package level. At the package level alone, both annotations only make sense with a checker framework that views packages as hierarchical constructs and ensures behavior similar to that of Java’s module system.
JSpecify is located under the coordinates
org.jspecify:jspecify:1.0.0
, and the module has no further
dependencies. However, the annotations are available not only during
compilation but also at runtime (@Retention(RUNTIME)
), so
the authors advise against using them in the provided
or
optional
scope. This naturally has consequences for use in
libraries: JSpecify becomes a transitive dependency. Anyone who, like
me, has cursed more than once about incompatible versions of frequently
used dependencies (commons-*, guava, older JSR-305 annotations, Jackson,
to name a few) is skeptical at first. As the author of several smaller
libraries, I don’t necessarily want to create more downstream ballast
and am very cautious about using third-party APIs in my API.
Personally, I would draw the following line for libraries: If a
library is only a small component of a larger framework and is not
usually affected by the actual application code, I would refrain from
using JSpecify. If the library is exposed and developers access
interfaces directly, we will find good arguments to make JSpecify part
of your API. Current IDEs will recognize these annotations and directly
draw attention to possible problems with null
values,
without any further tooling
The first step of using JSpecify will be getting into a defined
state. In my example project, which you find on Codeberg.org under /michael-simons/writing/src/branch/main/demos/jspecify,
I chose a single package-info.java
, to mark the whole
project, which consists of a single package, as
@NullMarked
:
@NullMarked
package ac.simons.javaspektrum.jspecify;
import org.jspecify.annotations.NullMarked;
Now all parameters are considered non-null by default. The following
example shows a calculator that adds integer values. The first parameter
must not be null
, but what about the other summands? The
example shows how nullness can be described precisely with JSpecify:
@Nullable Integer @Nullable ... others
means that the vargs
argument others
itself may be null
, as well as
any individual element of it:
package ac.simons.javaspektrum.jspecify;
import java.util.Objects;
import org.jspecify.annotations.Nullable;
public final class Calculator {
public Integer sum(Integer summand, @Nullable Integer @Nullable ... others) {
int sum = Objects.requireNonNull(summand, "One summand is required");
if (others != null) {
for (var other : others) {
+= Objects.requireNonNullElse(other, 0);
sum }
}
return sum;
}
}
The JSpecify manual covers the annotation of types, type parameters,
and generics in detail in the section Generics.
Together with the brief excursions into type theory, I find this section
outstanding and believe that it cannot be improved upon without making
the concept a genuine part of the language, as described in the
as-yet-unnumbered JEP “Null-Restricted and Nullable
Types”. It is striking here that both JSpecify and the Java
architects use the same theoretical background:
@Nullable String
and String?
are different
types than @NonNull String
and String!
. The
former are union types of null
and String
, the
latter are not.
Back to the example. All the following calls are valid:
var calc = new Calculator();
.sum(1);
calc.sum(1, 2, null, 3);
calc.sum(1, ((Integer[]) null)); calc
While IntelliJ and other IDEs will complain about
calc.sum(null);
that we are using literal null
with a @NonNull
parameter, but still happily compile and
run the following test successfully:
package ac.simons.javaspektrum.jspecify;
import org.junit.jupiter.api.Test;
import static org.assertj.core.api.Assertions.*;
class CalculatorTests {
@Test
void shouldRequireOneSummand() {
var calc = new Calculator();
assertThatNullPointerException()
.isThrownBy(() -> calc.sum(null))
.withMessage("One summand is required");
}
@Test
void shouldNotFailOnBadUsage() {
var calc = new Calculator();
assertThatNoException().isThrownBy(() -> calc.sum(1, (Integer[]) null));
}
}
JSpecify alone is only a tool. Annotations are used to replicate a
feature that should be part of the language. Without static analysis,
i.e., another tool in the build process, the annotations remain just
some arbitrary metadata for the Java compiler. Hopefully, this explains
something that the attentive reader might already have spotted: The
Calculator
class uses Objects.requireNonNull
to check the parameter summand
, which is actually declared
as @NonNull
, and also tests whether the varg parameter
others
is not null
as a whole and finally also
whether there is a null
value inside that
others
array.
Of course, there is not just one solution to this problem, but several: a nullness analyzer based on annotations that recognizes JSpecify annotations. One of these is uber/NullAway. NullAway is an Error Prone plugin. Error Prone itself augments the type analysis of the Java compiler so that it can detect more errors.
Error Prone itself is a great tool, but since it intervenes deeply in
the Java compiler process, it requires several packages from the
jdk.compiler
module that are not usually exported in modern
Java versions. The following listing shows the necessary exports for
Java 25:
--add-exports jdk.compiler/com.sun.tools.javac.api=ALL-UNNAMED
--add-exports jdk.compiler/com.sun.tools.javac.comp=ALL-UNNAMED
--add-exports jdk.compiler/com.sun.tools.javac.code=ALL-UNNAMED
--add-exports jdk.compiler/com.sun.tools.javac.main=ALL-UNNAMED
--add-exports jdk.compiler/com.sun.tools.javac.parser=ALL-UNNAMED
--add-exports jdk.compiler/com.sun.tools.javac.processing=ALL-UNNAMED
--add-exports jdk.compiler/com.sun.tools.javac.tree=ALL-UNNAMED
--add-exports jdk.compiler/com.sun.tools.javac.util=ALL-UNNAMED
Afterward, you can configure Error Prone together with NullAway as a Compiler-Plugin in your build-process. The following listing demonstrates how that is done with Maven:
build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<configuration combine.self="append">
<compilerArgs>
<arg>-XDcompilePolicy=simple</arg>
<arg>--should-stop=ifError=FLOW</arg>
<arg>-Xplugin:ErrorProne -Xep:NullAway:ERROR -XepOpt:NullAway:OnlyNullMarked=true</arg>
<compilerArgs>
</annotationProcessorPaths>
<path>
<groupId>com.google.errorprone</groupId>
<artifactId>error_prone_core</artifactId>
<version>2.38.0</version>
<path>
</path>
<groupId>com.uber.nullaway</groupId>
<artifactId>nullaway</artifactId>
<version>0.12.9</version>
<path>
</annotationProcessorPaths>
</configuration>
</plugin>
</plugins>
</build> </
The steps for Gradle are similar and documented right on the NullAway
GitHub-Project. The important parameters here are
-Xep:NullAway:ERROR -XepOpt:NullAway:OnlyNullMarked=true
:
All null
related issues should turn into actual compile
errors, but only for Modules and Packages that are
NullMarked
. The example
project comes with a profile that uses this configuration which can
be activated like this: mvn clean verify -DwithNullaway
. In
doing so, the test won’t compile anymore:
Compilation failure
[ERROR] /jspecify/src/test/java/ac/simons/javaspektrum/jspecify/CalculatorTests.java: [13,76] [NullAway] passing
@Nullable parameter 'null' where @NonNull is required
[ERROR] (see http://t.uber.com/nullaway )
If our library now could assume always being used with a Null-checker, we could get rid of our own null checks.
Kotlin has nullable and non-null types, and by default, all values
are non-null. Without JSpecify annotations in Calculator
,
this Kotlin test is valid code:
package ac.simons.javaspektrum.jspecify
import kotlin.test.Test
import kotlin.test.assertEquals
class KalkulatorTests {
@Test
fun sumShouldWork() {
val a: Int? = 1
val b: Int? = null
(2, Calculator().sum(a, b, 1)) }
assertEquals}
Since Calculator
is a Java class, all parameters are
nullable
without further information from the Kotlin
compiler’s perspective, so the correct type is Int?
.
However, once the package is marked as @NullMarked
, the
above test no longer compiles—without any further configuration or
plugins:
Kotlin: Argument type mismatch: actual type is 'Int?', but 'Int' was expected.
This is because the Kotlin compiler, since version 2.1.0, treats Java code annotated with JSpecify as null-safe types. As a result, introducing JSpecify annotations into a code base is a breaking change that requires not only a minor, but a major version bump. The correct call now is this:
().sum(a!!, b, 1) Calculator
Alternatively, a
can be declared Int
.
JSpecify is exceptionally well documented and specified, and is a fine example of how committee-organized standards can produce good results, especially when more than just one or two groups are represented.
In my current role, I have mixed feelings about JSpecify. It does
exactly what it’s supposed to do—especially in conjunction with
NullAway—but only then. As a library developer, I can’t do without null
checks. Whether I use Objects.requireNonNull
and indirectly
throw a NullPointerException
or prefer
IllegalArgumentException
is another discussion. If I wanted
to annoy the world, I would probably throw @lombok.NonNull
at it, as this annotation generates the above checks in the bytecode for
me.
From an application perspective, the situation is different, and I would recommend JSpecify and NullAway without reservation:
1 | Sir Antony Hoare | https://en.wikipedia.org/wiki/Tony_Hoare |
2 | Null pointer | https://en.wikipedia.org/wiki/Null_pointer |
3 | “Billion Dollar Mistake” | https://www.infoq.com/presentations/Null-References-The-Billion-Dollar-Mistake-Tony-Hoare/ |
4 | xkcd.com/927 | https://xkcd.com/927 |
5 | JSR 305: Annotations for Software Defect Detection | https://jcp.org/en/jsr/detail?id=305 |
6 | Project Lombok | https://projectlombok.org/features/NonNull |
7 | /michael-simons/writing/src/branch/main/demos/jspecify | https://codeberg.org/michael-simons/writing/src/branch/main/demos/jspecify |
8 | Generics | https://jspecify.dev/docs/user-guide/#generics |
9 | “Null-Restricted and Nullable Types” | https://openjdk.org/jeps/8303099 |
10 | uber/NullAway | https://github.com/uber/NullAway |
11 | Error Prone | https://errorprone.info |
Source jspecify-and-nullaway-a-fresh-take-on-nullsafety-in-the-java-world.md