Groovy 3 helps you write less, but more secure code. Today I want to show you one of the features added in the latest release - @NullCheck annotation.

What is @NullCheck?

The @groovy.transform.NullCheck belongs to the category of annotations that trigger specific AST transformations at the compilation time. This specific annotation can be added to class, constructor, or method. When it is present, it adds if-statement that checks if a variable (or variables) is not null, and throws IllegalArgumentException otherwise.

Here is Groovy =<2.5 class example that does all null checks manually.

Listing 1. Groovy 2.5 solution.
import groovy.transform.CompileStatic

@CompileStatic
class Foo {
    private final String str

    Foo(final String str) {
        if (str == null) { (1)
            throw new IllegalArgumentException("str cannot be null")
        }

        this.str = str
    }

    String bar(final BigDecimal value) {
        if (value == null) { (2)
            throw new IllegalArgumentException("value cannot be null")
        }

        return str.toUpperCase() + " = " + value.toString()
    }
}

assert new Foo("test").bar(BigDecimal.TEN) == "TEST = 10"

new Foo("test").bar(null) (3)
1Explicit null check in the constructor.
2Explicit null check in the method body.
3Calling bar(null) to get IllegalArgumentException.

When you run such a Groovy script, you will see IllegalArgumentException as expected.

$ groovy test.groovy
Caught: java.lang.IllegalArgumentException: value cannot be null
java.lang.IllegalArgumentException: value cannot be null
	at Foo.bar(test.groovy:17)
	at Foo$bar.call(Unknown Source)
	at test.run(test.groovy:26)

When using Groovy 3 (and higher) and @NullCheck annotation we can get replace all explicit checks with a single annotation. The AST transformation that runs at the compile-time produces the same bytecode as in the explicit Groovy 2.5 use case.

Listing 2. Groovy 3+ solution.
import groovy.transform.CompileStatic
import groovy.transform.NullCheck

@CompileStatic
@NullCheck (1)
class Foo {
    private final String str

    Foo(final String str) {
        this.str = str
    }

    String bar(final BigDecimal value) {
        return str.toUpperCase() + " = " + value.toString()
    }
}

assert new Foo("test").bar(BigDecimal.TEN) == "TEST = 10"

new Foo(null).bar(BigDecimal.ONE) (2)
1@NullCheck at the class level affects all constructors and methods.
2This time we call a constructor with a null argument to get IllegalArgumentException.

Running the following example in the command line produces the expected result.

$ groovy test.groovy
Caught: java.lang.IllegalArgumentException: str cannot be null
java.lang.IllegalArgumentException: str cannot be null
	at Foo.<init>(test.groovy)
	at test.run(test.groovy:20)

Limitations

So far I have noticed one minor limitation - the @NullCheck annotation doesn’t work when combined with @Canonical or @Immutable annotations. If supported, it would allow us to skip constructor definition and simplify the code to something like this.

import groovy.transform.CompileStatic
import groovy.transform.NullCheck
import groovy.transform.Immutable

@CompileStatic
@NullCheck
@Immutable
class Foo {
    final String str

    String bar(final BigDecimal value) {
        return str.toUpperCase() + " = " + value.toString()
    }
}

assert new Foo("test").bar(BigDecimal.TEN) == "TEST = 10"

new Foo(null).bar(BigDecimal.ONE) (1)

In this case, we will get NullPointerException thrown by str.toUpperCase(), because there was no null-check in the generated constructor.

But I’m optimistic, and I think this limitation may get solved in one of the future Groovy updates. I definitely add @NullCheck to the @CompileStatic annotations arsenal I use on a daily basis.

Szymon Stepniak

Groovista, Upwork's Top Rated freelancer, Toruń Java User Group founder, open source contributor, Stack Overflow addict, bedroom guitar player. I walk through e.printStackTrace() so you don't have to.