Livre-se de java.lang.NullPointerException com o Checker Framework

O Checker Framework é uma ferramenta que nos ajuda a detectar e prevenir erros em nossas aplicações. Ele oferece vários verificadores que fazem uma análise em nosso código em busca de bugs. Além disso, também podemos escrever nossos próprios plug-ins. Um desses verificadores é o Nullness Checker, que nos fornece anotações para eliminar a chance de qualquer NullPointerException ser lançada em nossa aplicação. Ele verifica o código em tempo de build, fazendo com que a construção falhe se qualquer NullPointerException possível for encontrada. Nesta postagem, vamos aprender a usar o Nullness Checker.

O código completo que será criado neste post pode ser encontrado no GitHub.

Instalação

Primeiramente temos que adicionar a dependência em nosso 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>

Então vamos adicionar ou editar o maven-compiler-plugin que se encontra na seção <plugins>

<?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>

Nesse ponto, estamos prontos para usar as anotações e evitar NullPointerExceptions.

Usando Anotações

As anotações mais usadas com as quais podemos começar são:

@Nullable – indica uma variável que pode ser nula

@NonNull – indica uma variável que nunca vai ser nula

Anotando Nossos Primeiros Atributos

Vamos criar uma classe chamada MyClass com dois atributos e anotá-los com @Nullable e @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;
    }

}

Nesse ponto, se tentarmos construir nosso projeto, a construção irá falhar devido a dois erros:

  • 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

O primeiro está nos dizendo que um campo não nulo-não está sendo inicializado pelo construtor. Se não fizermos isso, o objeto será criado com o campo não-nulo tendo seu valor nulo.

O segundo está nos dizendo que em getNumber() estamos retornando o valor de um atributo que pode ser nulo e o método não está anotado como tal (por default, a ausência de anotações indica que o método está implicitamente anotado com @NonNull).

Corrigindo Os Erros

Para corrigir o primeiro erro, vamos criar um construtor que inicializa o atributo ‘text’:

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

Para o Checker Framework, isso é suficiente para eliminar o primeiro erro. Mas como estou usando o Intellij IDEA e o mesmo mostra um warning dizendo que o parâmetro do construtor também pode ser anotado com @NonNull. Então vou fazer isso.

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

Para corrigir o segundo erro, temos que anotar o tipo de retorno do método getNumber () com @Nullable. Fazendo isso, estamos dizendo ao chamador do método que ele pode retornar um valor nulo.

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

Isso é o suficiente para eliminar o segundo erro. Agora nossa classe deve estar assim:

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;
    }

}    

Agora, quando usamos um objeto do tipo MyClass, o Checker Framework garante que não vamos esquecer de verificar os valores nulos.

A construção irá falhar se fizermos o seguinte:

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()

O Checker Framework está nos dizendo que uma NullPointerException pode ser lançada por getNumber(). Mas observe que ele nos permite usar o retorno de getText() diretamente porque sabe que o método nunca retornará um valor nulo, pois anotamos o retorno como @NonNull.

Portanto, temos que verificar se o valor retornado de getNumber() é nulo antes de podermos usá-lo.

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");
    }
}

Agora podemos ter certeza de que nosso código não lançará nenhuma NullPointerException.

Conclusão

O Checker Framework é uma ótima ferramenta que nos ajuda a eliminar erros inesperados em nossas aplicações. O Nullness Checker é apenas um dos muitos verificadores fornecidos. Você pode ler a documentação em https://checkerframework.org para se aprofundar na ferramenta.

O código que criamos nesta postagem está no GitHub.

Marcações:

Deixe uma resposta

O seu endereço de e-mail não será publicado. Campos obrigatórios são marcados com *