diff --git a/_data/authors.yml b/_data/authors.yml index 15e274f8f3..d8eddd8c4d 100644 --- a/_data/authors.yml +++ b/_data/authors.yml @@ -1005,9 +1005,7 @@ authors: picture: picture.jpg magnussmith: name: "Magnus Smith" - author-summary: "
I'm a senior developer at Scott Logic, based in Newcastle. I've been involved with Java development for the last fifteen years and I'm keenly interested in modern JVM technologies." - twitter-handle: magnus2025 - twitter-url: "http://www.twitter.com/magnus2025" + author-summary: "
Lead Developer at Scott Logic, based in Newcastle. I've 25 years working with Java and passionate about modern JVM technologies.
" picture: picture.jpg hashbyha: name: "Hugh Ashby-Hayter" diff --git a/_posts/2024-12-18-taming-nullness-in-java-with-jspecify.md b/_posts/2024-12-18-taming-nullness-in-java-with-jspecify.md new file mode 100644 index 0000000000..23551faf12 --- /dev/null +++ b/_posts/2024-12-18-taming-nullness-in-java-with-jspecify.md @@ -0,0 +1,445 @@ +--- +title: Using JSpecify 1.0 to Tame Nulls in Java +date: 2024-12-18 00:00:00 Z +categories: +- Tech +tags: +- Java +- Intellij +author: magnussmith +summary: This post is designed for Java developers who want to adopt JSpecify for consistent nullability handling in their projects. By following the steps and examples, you should be able to set up and utilize the core JSpecify annotations effectively in your codebase. +image: magnussmith/assets/java.svg +--- + +## Introduction + + +In the Java ecosystem, dealing with null values has always been a source of confusion and bugs. A null value can represent various states: the absence of a value, an uninitialized object, or even an error. However, there has never been a consistent, standardized approach for annotating and ensuring null-safety at the language level. + +Nullability annotations like `@Nullable` and `@NonNull` are often used, but they're not part of the core Java language, leading to inconsistencies across libraries and frameworks. Some use the defunct `JSR-305` `@Nullable` from the javax.annotation package, while others prefer `@NotNull` from `org.jetbrains.annotations`. However, these solutions are often inconsistent and can lead to confusion or errors in codebases. + +JSpecify is a specification that provides a standardized approach to annotating nullability in Java, offering a set of annotations designed to improve code clarity and prevent null-related bugs. The goal is to eventually make these annotations part of the standard Java platform. + + +## The Four Nullness Annotations +JSpecify introduces four key annotations to express nullness: + + - `@Nullable`: Indicates that a variable, parameter, or return value can be null. +- `@NonNull`: Indicates that a variable, parameter, or return value cannot be null. +- `@NullMarked`: Marks a package or class that you're annotating to indicate that the remaining unannotated type usages are not nullable. This reduces the noise from annotation verbosity. +- `@NullUnmarked`: Explicitly marks a package or class as not using JSpecify's nullness annotations as the default. This is used for exceptions to `@NullMarked` packages. + +The goal is to allow for more predictable null handling, minimizing the need for runtime null checks and making nullness explicitly part of the contract of methods and fields. + + +This post covers the process of setting up JSpecify 1.0 in your project, configuring IntelliJ IDEA and Gradle, and how to effectively use the four core annotations: `@Nullable`, `@NonNull`, `@NullMarked`, and `@NullUnmarked`. + +## Applying JSpecify Incrementally to a Legacy Project +Let's imagine a very simplistic User class: + +~~~ java +public class User { +private String name; +private String address; + + public User(String name) { + this.name = name; + } + + public String getName() { + return name; + } + + public String getAddress() { + return address; // Could be null + } + + public void setAddress(String address) { + this.address = address; + } + + public String getFormattedAddress() { + return getAddress().toUpperCase(); // Potential NPE! + } +} +~~~ + +This User class has a glaring potential NPE in `getFormattedAddress()`. Let's use JSpecify to address this. + +## Step 1: Add JSpecify Dependency + +To integrate JSpecify into a Gradle project, add the JSpecify annotation library as a dependency. As of version 1.0, JSpecify is available in Maven Central, so it’s straightforward to include. + +In your `build.gradle` (or `build.gradle.kts`), add the following dependency: + +First, in your `build.gradle` (or `build.gradle.kts`) add the JSpecify dependency to your project (using Gradle): + +~~~ gradle +dependencies { + implementation 'org.jspecify:jspecify:1.0.0' +} +~~~ + + + +## Step 2: Introduce @Nullable and @NonNull + +We can start by annotating the getAddress() method: + +~~~ java +import org.jspecify.nullness.Nullable; +import org.jspecify.nullness.NonNull; + +public class User { +// ... other code + + public @Nullable String getAddress() { + return address; + } + + public void setAddress(@Nullable String address) { + this.address = address; + } + + public @NonNull String getName() { return name; } + + public String getFormattedAddress() { + String address = getAddress(); + if (address != null) { + return address.toUpperCase(); + } else { + return ""; // Or handle null appropriately + } + } +} +~~~ + +Now, a static analysis tool (like IntelliJ's built-in inspection or Error Prone) will warn us about the potential NPE in getFormattedAddress() if we don't handle the null case. We've added a null check to fix it. + + +## Step 3: Using @NullMarked + +To reduce verbosity, especially in larger classes or packages, use `@NullMarked`: + +~~~ java +import org.jspecify.nullness.Nullable; +import org.jspecify.nullness.NullMarked; + + +@NullMarked +public class User { + private String name; // Treated as @NonNull because of @NullMarked + private @Nullable String address; + + // ... other code +} + +~~~ +Now, all unannotated types within the User class are treated as `@NonNull`, unless explicitly marked with `@Nullable`. + +We can also apply `@NullMarked` and `@NullUnmarked` at the Package and Module Levels. If you needed to exempt a class from the null marked package you would use `@NullUnmarked` on the class you need to exempt. + +You would place `@NullMarked` or `@NullUnmarked` in a `package-info.java` file to affect the entire package. + +### At the Package Level + +You can place `@NullMarked` or `@NullUnmarked` in a package-info.java file to affect the entire package. + +~~~ java + // package-info.java + @NullMarked + package com.example.myapp; +~~~ + +All classes in the `com.example.myapp` package will now assume non nullable types by default unless explicitly overridden. + +### At the Module Level + +If your project is modularized, you can also use these annotations at the module level by adding `@NullMarked` or `@NullUnmarked` to the `module-info.java` file. + +~~~ java + // module-info.java + @NullMarked + module com.example.myapp { + requires java.base; + // .... other require details + exports com.example.myapp; + } +~~~ + +This will make sure all types within the module are non nullable by default. + +Starting at the class level annotations and then moving to package or module annotations provides a way to apply nullness analysis in stages to what may be a large existing project. + + +## IntelliJ Null Analysis + +Once JSpecify is included in your project, you need to ensure that IntelliJ IDEA is properly set up to recognize and process these annotations. + +IntelliJ IDEA has built-in support for JSpecify. Enable "Nullness annotations" under `Settings/Preferences > Editor > Inspections > Java -> Probable bugs` to see warnings about potential NPEs based on your JSpecify annotations. + +![intellij_null_insp.png]({{ site.github.url }}/magnussmith/assets/intellij_null_insp.png "Intellij Null Inspection") +If you want to use jspecify notifications in generated code then you can set that here + +![intelij_null_annot.png]({{ site.github.url }}/magnussmith/assets/intelij_null_annot.png "Source Generated Annotations") + + + + + + + +## Implementing detection with Gradle using ErrorProne and Nullaway + +### Error Prone +[Error Prone](https://errorprone.info/index) is a static analysis tool for Java that catches common programming mistakes at compile time. Instead of just producing compiler warnings, Error Prone directly integrates with the Java compiler to generate more informative and precise error messages. It goes beyond simple syntax checking by analyzing the abstract syntax tree (AST) of your code to identify problematic patterns. + +#### What Error Prone Does: + +- `Finds common bugs`: Detects a wide range of errors, including null pointer dereferences, incorrect equality checks, misuse of collections, and many more. +- `Provides clear error messages`: Offers specific, actionable error messages that explain the problem and often suggest how to fix it. +- `Integrates with the compiler`: Works seamlessly with the Java compiler, so you don't need a separate tool or process. +- `Extensible`: Allows you to write custom checks to enforce project-specific coding standards. + +Error Prone can be used in many useful ways, even fixing some issues automatically. I shall cover that in more depth in a future post. + +### NullAway +[NullAway](https://github.com/uber/NullAway) is a static analysis tool built on top of Error Prone specifically designed to detect null pointer dereferences. It leverages annotations (such as JSpecify's `@Nullable` and @NonNull) to understand the nullness constraints of your code and identify potential NPEs. + +#### What NullAway Does: + +- `Focuses on nullness`: Specifically targets null pointer dereferences, providing highly accurate null analysis. +- `Annotation-driven`: Uses annotations to understand nullability, allowing you to express your intent clearly. +- `Integrates with Error Prone`: Builds upon Error Prone's infrastructure for seamless integration with the Java compiler. +- `Configurable`: Offers various options to fine-tune the analysis and handle specific scenarios. + + +## Using Error Prone and NullAway with Gradle +Here's how to integrate Error Prone and NullAway into your Gradle build: + +~~~ gradle + +plugins { + id("net.ltgt.errorprone") version "4.1.0" + id("net.ltgt.nullaway") version "2.1.0" +} + + +dependencies { + implementation('org.jspecify:jspecify:1.0.0') + errorprone('com.google.errorprone:error_prone_annotations:2.36.0') + annotationProcessor('com.google.errorprone:error_prone_core:2.36.0') +} + +tasks.withType(JavaCompile).configureEach { + options.errorprone.nullaway { + error() + // This will default this package to @NullMarked + // If you don't want any specifically marked then need to pass empty string + annotatedPackages.add("your.basepackage") + } + // Include to disable NullAway on test code + if (name.toLowerCase().contains("test")) { + options.errorprone { + disable("NullAway") + } + } + // Optional: configure Error Prone to fail the build on errors + options.errorprone.allErrorsAsWarnings.set(true) + options.errorprone.disableWarningsInGeneratedCode.set(true) + options.errorprone.errorproneArgs.addAll( + "-Xep:NullAway:WARN", // Enable NullAway with WARN severity + "-Xep:CheckReturnValue:WARN", // Example of another Error Prone check + "-Xep:UnusedVariable:WARN", + "-Xep:UnusedMethod:WARN", + "-Xep:EqualsHashCode:WARN", + "-Xlint:-processing" // Suppress annotation processing warnings + ) +} + +~~~ + +This configures Error Prone (with NullAway) to run during compilation, providing more robust null analysis that could be enhanced to be used with a gradle profile as part of automated CI builds. + + + +## JSpecify with Generics +JSpecify also works with generics providing some more advanced capabilities + +~~~ java + +import org.jspecify.nullness.NonNull; +import org.jspecify.nullness.Nullable; +import org.jspecify.nullness.NullMarked; + +@NullMarked +public class Result