JSpecify 1.0.0 and Nullability in Java

MMS Founder
MMS Ben Evans

Article originally posted on InfoQ. Visit InfoQ

The JSpecify collective has made their first release, JSpecify 1.0.0. The group’s mission is to define common sets of annotation types for use in JVM languages, to improve static analysis and language interoperation.

The projects and groups in the consensus include:

  • OpenJDK
  • EISOP
  • PMD
  • Android, Error Prone, Guava (Google)
  • Kotlin, IntelliJ (JetBrains)
  • Azure SDK (Microsoft)
  • Sonar
  • Spring

By participating in JSpecify, the members guarantee their projects will be backwards compatibility at source level. This means that they will not rename the annotations in the jar, move them or make other changes that would cause code compilation to fail due to an update. In other words, it is safe to depend on it, and it won’t be changed in any way that breaks application code.

This initial release focuses on the use of type-use annotations to indicate the nullability status of usages of static types. The primary use cases are expected to be variables, parameters and return values, but they can be used anywhere that tracking nullness makes conceptual sense.

Some possibilities do not pass the “conceptual sense” test however – e.g. you still can’t write things like class Foo extends @Nullable Bar.

Background on nullability and nullness checkers

The subject of nullability has long been a topic of debate in the Java ecosystem. The idea is that developers should be better insulated from NPEs, as large swathes of use cases that currently could cause NPEs will be ruled out at build time.

However, this promising idea has been held up by some unfortunate facts, specifically that Java has both:

  • A long history which largely predates viable concepts of non-nullable values in programming languages.
  • A strong tradition of backwards compatability.

It has therefore been difficult to see a path forward that allows null-awareness to be added to the language in a consistent and non-breaking fashion.

Nevertheless, attempts have been made — the most famous is probably JSR 305 which ran for several years, but eventually became Dormant in 2012 without being completed and fully standardized.

After the failure of that effort, various companies and projects pressed ahead with their own versions of nullability annotations. Some of these projects were trying to solve primarily for their own use cases, but others are intended to be broadly usable by Java applications.

Of particular interest is the Kotlin language, which has built null-awareness into the type system. This is achieved by assuming that, for example, String indicates a value that cannot be null, and requiring the developer to explicitly indicate nullability as String?. This is mostly very successful, at least as long as a program is written in pure Kotlin.

Java developers sometimes exclaim that they “just want what Kotlin has”, but as the Kotlin docs make clear there are a number of subtleties here, especially when interoperating with Java code.

Currently, in the pure Java space, most current projects approach nullability by treating the null status as extra information to be processed via a build-time checker (static analysis) tool. By adding additional information in the form of annotations, then the checker aims to reduce or eliminate NPEs from the program the analysis was run on.

Different projects have different philosophies about how much of a “soundness guarantee” they want to offer. Soundness comes at a cost – more complexity, more effort to migrate your codebase, more false positives to deal with, so not every developer will want full and complete guarantees.

JSpecify represents an attempt to unify these approaches, and the initial goals are quite modest – version 1.0.0 defines only four annotations:

  • @Nullable
  • @NonNull
  • @NullMarked
  • @NullUnmarked

The first two can be applied to specific type usage (e.g. parameter definition or method return values), while the second pair can be broadly applied. Together, they provide usable declarative, use-site null-awareness for Java types.

One general common use case is to add @NullMarked to module, package or type declarations that you’re adding nullability information to. This can save a lot of work, as it indicates that any remaining unannotated type usages are not nullable. Then, any exceptional cases can be explicitly marked as @Nullable – and this arrangement is the sensible choice for a default.

The intent is for end users to adopt (or continue to use) one of the participating projects – and different projects have different levels of maturity. For example, IntelliJ supports JSpecify annotations, but generics are still not fully supported yet.

A reference checker is also available, but this is not really intended for use by application developers, but instead is aimed at the projects participating in JSpecify.

There is also a FAQ covering the deeper, more technical aspects of the design.

InfoQ contacted one of the key developers working on JSpecify – Juergen Hoeller (Spring Framework project lead).

InfoQ: Please tell us about your involvement in JSpecify and nullability:

Hoeller: Together with Sebastien Deleuze, I’ve been working on the nullability design in Spring Framework 5 which led to our participation in JSpecify.

InfoQ: Can you explain how JSpecify is used in your projects and why it’s important to them?

Hoeller: The Spring Framework project and many parts of the wider Spring portfolio have adopted a non-null-by-default design in 2017, motivated by our Kotlin integration but also providing significant benefits in Java-based projects (in combination with IntelliJ IDEA and tools like NullAway).

This is exposed in our public API for application-level callers to reason about the nullability of parameters and return values. It also goes deep into our internal codebase, verifying that our nullability assumptions and assertions match the actual code flow. This is hugely beneficial for our own internal design and maintenance as well.

The original JSR 305 annotations were embraced quite widely in tools, despite never officially having been finished. Spring’s own annotations use JSR 305 as meta-annotations for tool discoverability, leading to immediate support in IntelliJ IDEA and Kotlin.

Our involvement in JSpecify started from that existing arrangement of ours since we were never happy with the state of the JSR 305 annotations and wanted to have a strategic path for annotation semantics and tool support going forward, allowing us to evolve our APIs and codebase with tight null safety.

InfoQ: It’s taken 6 years to get to 1.0.0. Why do you think that it’s taken so long?

Hoeller: While our current use cases in Spring are pretty straightforward, focusing on parameter and return values as well as field values, the problem space itself is complex overall. The JSpecify mission aimed for generics support which turned out to be rather involved. JSpecify 1.0 considers many requirements from many stakeholders, and it is not easy to reach consensus with such a wide-ranging collaborative effort.

InfoQ: What does the future look like, if JSpecify is successful?

Hoeller: For a start, JSpecify is well-positioned to supersede JSR 305 for tool support and for common annotations in open source frameworks and libraries. It is critical to have such support on top of the currently common baselines such as JDK 17 since large parts of the open source ecosystem will remain on such a baseline for many years to come. Even on newer Java and Kotlin versions over the next few years, JDK 17 based frameworks and libraries will still be in widespread use, and JSpecify can evolve along with their needs.

InfoQ: What’s next for JSpecify to tackle?

Hoeller: From the Spring side, we need official meta-annotation support so that we can declare our own nullability annotations with JSpecify meta-annotations instead of JSR 305 meta-annotations.

InfoQ: Thank you!

About the Author

Subscribe for MMS Newsletter

By signing up, you will receive updates about our latest information.

  • This field is for validation purposes and should be left unchanged.