Null-Restricted and Nullable Types for Java

MMS Founder
MMS Ben Evans

Earlier this week, we reported on the release of version 1.0.0 of the JSpecify project. This release focuses on providing type-use annotations to indicate the nullability status of usages of static types.

Related to this subject, Draft JEP 8303099 was recently made public. This JEP discusses Null-Restricted and Nullable Types, and aims to bring optional nullness-marking to the Java language.

The intent is to add markers – not just annotations – to a type use to specify whether the permissible value set for that use includes null or not. It is important to bear in mind that the proposal is in a very early stage of development (e.g. it doesn’t have an official JEP number yet), so the syntax may well change. Having said this, for now Kotlin-like markers are used, so that, for a type Foo, there are three possibilities for how the type can be used:

  • Foo! is null-restricted – the acceptable values for this use of the type do not include null
  • Foo? is nullable – the acceptable values for this use of the type explicitly includes null
  • Foo does not specify whether or not null is acceptable

The use of bare Foo remains the default, as the meaning of existing code should not change when it’s compiled.

Currently, the proposal calls for every type use to be annotated, i.e. there is no way to mark an entire class or module as null-restricted (as would be possible in JSpecify), although this capability may be added later.

This new feature introduces nullness conversions (similar to widening and unboxing conversions). For example, any of these sorts of assignment are permissible:

  • Foo! to Foo?
  • Foo! to Foo
  • Foo? to Foo
  • Foo to Foo?

as they represent a loosening of constraints – e.g. any null-restricted value can be represented in a nullable use of the type.

There are also narrowing nullness conversions, such as:

  • Foo? to Foo!
  • Foo to Foo!

These could cause runtime errors, e.g. by trying to load a null from a Foo? into a Foo!. The general strategy here is to treat these cases as compile-time warnings (but not errors) and to include a runtime check that throws a NullPointerException if the nullness bound if violated.

Note that these are basically the easy cases, and more complex cases are possible. For example, when dealing with generics the compiler may encounter situations such as type arguments whose nullness is inconsistent with their declared bounds.

The introduction of null markers provides additional compile-time safety, and allows for a gradual adoption of the markers – first by defining the nullness of types, and then seeking to eliminate compile-time warnings.

InfoQ spoke to Kevin Bourrillion (founder of Google’s core libraries team and now a member of Oracle’s Java language team) — to get more details about the project.

InfoQ: Can you explain your background with the nullness efforts in Java?

Bourrillion: I co-founded the JSpecify group, and have been one of the main “designers” (defined as “person who bears the burden of driving insanely complicated design decisions to consensus somehow”). I’ve now moved to Oracle but remain involved in approximately the same ways.

InfoQ: How does this JEP overlap with the JSpecify work?

Bourrillion: Eventually we will have a Java with support for nullness markers. JSpecify has done the hard work of nailing down the semantic meanings of the annotations very precisely. This means that whatever upgrade timetable projects choose to adopt will be in a really good position to migrate from JSpecify to language-level nullness markers – in fact that migration should be highly automatable.

InfoQ: There seem to be some similarities between Draft JEP 8303099 and the way that generics was added, way back in Java 5. Is that accurate? This is largely a compile-time mechanism, which is mostly erased at bytecode level, isn’t it?

Bourrillion: Yes, there are some useful parallels there. We think of type erasure and the spectre of “heap pollution” as being unfortunate concessions, but that is what made the feature so *adoptable*. That’s why you almost never have to see a raw type anymore today (I hope!). Now in our case, null pollution will be part of our reality for a long time, but that’s okay! Today it’s all null pollution.

And yes, like generic type information, your nullness annotations are available at runtime via reflection, but are not involved in runtime type checking. I will be interested to see whether anyone builds a bytecode-instrumenter that injects null checks based on our annotations; I can see reasons that might be really useful and reasons it might not be worth the trouble; we’ll have to see.

InfoQ: Null-restriction is also an important topic for Project Valhalla, isn’t it? What can you share about the interaction between this Draft JEP and the ongoing work in that area?

Bourrillion: This is really the same question; Valhalla is just going to build on from that JEP draft you cited. Knowing what can’t be null will help the VM optimize those indirections away.

InfoQ: In the mid to long term, JSpecify should be able to provide an on-ramp to language-level nullability support in Java. This is similar to how it can already be used to alignment with Kotlin’s nullability support. How would you recommend readers go adopt adopting JSpecify today?

Bourrillion: It’s just the JSpecify jar that’s 1.0.0. The specification, which dictates the very precise meaning of the annotations, is still subject to (slight and subtle) changes. So if you put in a lot of work to annotate your codebase today, your code won’t suddenly stop compiling correctly, but after some small spec revisions you might find you want to remove a couple `@Nullable`s here or add a few there.

If adopting nullness analysis in your toolchain today is just too time-consuming because of all the existing warnings you have to clean up, it’s actually a perfectly reasonable approach to spray `@SuppressWarnings` however broadly you need! It’s a bit ugly, but that’s something you can just clean up incrementally over time. Even if that takes a long time, the point is that *new* code will start getting checked today, and that’s the most important code to check anyway.

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.

JSpecify 1.0.0 and Nullability in Java

MMS Founder
MMS Ben Evans

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.