Get Rid of java.lang.NullPointerException with Checker Framework

Checker Framework is a tool that helps us to detect and prevent errors in our applications.
It has several checkers that verify our code in search of bugs, and we can also write our compiler plug-ins.
One of these checkers is the Nullness Checker, which provides us with annotations to eliminate the chance of any NullPointerException
to be thrown in our application. It checks our code at build time causing it to fail if any possible NullPointerException is found.
In this post, we will learn how to use the Nullness Checker.

The complete code that will be created in this post can be found at
GitHub.

Installation

First of all, we have to add the dependency in our pom.xml:

<dependencies>
    <!-- Other dependencies -->
    <dependency>
        <groupId>org.checkerframework</groupId>
        <artifactId>checker-qual</artifactId>
        <version>3.3.0</version>
    </dependency>
    <!-- If using JDK 8, add the following additional dependency. -->
    <dependency>
        <groupId>com.google.errorprone</groupId>
        <artifactId>javac</artifactId>
        <version>9+181-r4173-1</version>
    </dependency>
    <!-- Other dependencies -->
</dependencies>

Then we will add or edit the maven-compiler-plugin within the <plugins> section

<?xml version="1.0" encoding="UTF-8"?>
<plugin>
    <artifactId>maven-compiler-plugin</artifactId>
    <version>3.8.1</version>
    <configuration>
        <fork>true</fork>
        <!-- Must fork or else JVM arguments are ignored. -->
        <!-- If using JDK 8, add source and target. -->
        <source>1.8</source>
        <target>1.8</target>
        <!-- If using JDK 11, remove source and target and uncomment "release" below. -->
        <!-- <release>11</release> -->
        <compilerArguments>
            <Xmaxerrs>10000</Xmaxerrs>
            <Xmaxwarns>10000</Xmaxwarns>
        </compilerArguments>
        <!-- Without showWarnings and verbose, maven-compiler-plugin may not show output. -->
        <showWarnings>true</showWarnings>
        <verbose>true</verbose>
        <annotationProcessorPaths>
            <path>
                <groupId>org.checkerframework</groupId>
                <artifactId>checker</artifactId>
                <version>3.4.0</version>
            </path>
        </annotationProcessorPaths>
        <annotationProcessors>
            <!-- Add all the checkers you want to enable here -->
            <annotationProcessor>org.checkerframework.checker.nullness.NullnessChecker</annotationProcessor>
        </annotationProcessors>
        <compilerArgs>
            <!-- If using JDK 8, use only the two arguments immediately below. -->
            <arg>-J-Xbootclasspath/p:${errorProneJavac}</arg>
            <!-- If using JDK 11, remove the two arguments above, remove the
                space in the one below, and uncomment it. -->
            <!-- <arg>-J- -add-opens=jdk.compiler/com.sun.tools.javac.comp=ALL-UNNAMED</arg> -->
            <!-- Optionally, -Awarns turns type-checking errors into warnings. -->
            <!-- <arg>-Awarns</arg> -->
        </compilerArgs>
    </configuration>
</plugin>

At this point, we are ready to use the annotations and avoid NullPointerExceptions.

Using Annotations

The most used annotations that we can start with are:

@Nullable – indicates a variable that can be null

@NonNull – indicates a variable that will never be null

Annotating Our First Fields

Let’s create a class called MyClass with two attributes and annotate them with @Nullable and @NonNull.

import org.checkerframework.checker.nullness.qual.NonNull;
import org.checkerframework.checker.nullness.qual.Nullable;

public class MyClass {

    private @Nullable Integer number;

    private @NonNull String text;

    public Integer getNumber() {
        return number;
    }

    public void setNumber(Integer number) {
        this.number = number;
    }

    public String getText() {
        return text;
    }

    public void setText(String text) {
        this.text = text;
    }

}

At this point, if we try to build our project, the build will fail with two errors:

  • Error:(6, 8) java: [initialization.fields.uninitialized] the constructor does not initialize fields: text
  • Error:(13, 16) java: [return.type.incompatible] incompatible types in return.

    • type of expression: @Initialized @Nullable Integer
    • method return type: @Initialized @NonNull Integer

The first one is telling us that a non-null field is not being initialized by the constructor. If we don’t do that, the object will be created
with the non-null field having a null value.

The second one is telling us that we’re returning a nullable field in getNumber() and the method isn’t annotated as returning a nullable value
(by default, if something isn’t annotated it is implicitly annotated with @NonNull).

Correcting the errors

To correct the first error, let’s create a constructor that initializes the ‘text’ field:

public MyClass(String text) {
    this.text = text;
}

For Checker Framework, that is enough to eliminate the first error. But I’m using IntelliJ IDEA and it shows a warning saying that the constructor parameter also might be annotated with @NonNull. So, I’ll do it.

public MyClass(@NonNull String text) {
    this.text = text;
}

To correct the second error, we have to annotate the return type of the getNumber() method with @Nullable. Doing that we are saying to the method caller that it might return a null value.

public @Nullable Integer getNumber() {
    return number;
}

That’s enough to eliminate the second error. Now our class should look like this:

import org.checkerframework.checker.nullness.qual.NonNull;
import org.checkerframework.checker.nullness.qual.Nullable;

public class MyClass {

    private @Nullable Integer number;

    private @NonNull String text;

    public MyClass(@NonNull String text) {
        this.text = text;
    }

    public @Nullable Integer getNumber() {
        return this.number;
    }

    public void setNumber(@Nullable Integer number) {
        this.number = number;
    }

    public @NonNull String getText() {
        return this.text;
    }

    public void setText(@NonNull String text) {
        this.text = text;
    }

}    

Now when we use an object of MyClass type, the Checker Framework guarantees that we won’t forget to check for null values.

The build will fail if we do this:

public void useMyClass(MyClass object) {
    if (object.getNumber() > 5) {
        System.out.println("The number is greated than 5");
    }

    if (object.getText().length() > 5) {
        System.out.println("The text has a length greater than 5");
    }
}
Error:(6, 29) java: [unboxing.of.nullable] unboxing a possibly-null reference object.getNumber()

The Checker Framework is telling us that a NullPointerException might be thrown by getNumber(). But note that it lets us use the return of getText()
directly because it knows that it will never be null since we annotated the return as @NonNull.

So we have to check if the returned value of getNumber() is null before we can use it.

public void useMyClass(MyClass object) {
    Integer number = object.getNumber();
    if (number != null && number > 5) {
        System.out.println("The number is greated than 5");
    }

    if (object.getText().length() > 5) {
        System.out.println("The text has a length greater than 5");
    }
}

Now we can be sure that our code won’t throw any NullPointerException.

Conclusion

The Checker Framework is a great tool that helps us to eliminate unexpected errors in our applications. The Nullness Checker is just one of many provided checkers.
You can read the documentation at https://checkerframework.org to get deep into the framework.

The code that we created in this post is in GitHub.

Leave a Reply

Your email address will not be published. Required fields are marked *