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)

Combining @NullCheck with other annotations

Starting from Groovy 3.0.2, the @NullCheck annotation offers includeGenerated option. This option allows to use the annotation in combination with other AST transformations like @Immutable or @TupleConstructor.

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

@CompileStatic
@NullCheck(includeGenerated = true)
@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)

Output:

$ groovy test.groovy
Caught: java.lang.IllegalArgumentException: args cannot be null
java.lang.IllegalArgumentException: args cannot be null
	at Foo.<init>(test.groovy)
	at java.base/jdk.internal.reflect.NativeConstructorAccessorImpl.newInstance0(Native Method)
	at java.base/jdk.internal.reflect.NativeConstructorAccessorImpl.newInstance(NativeConstructorAccessorImpl.java:62)
	at java.base/jdk.internal.reflect.DelegatingConstructorAccessorImpl.newInstance(DelegatingConstructorAccessorImpl.java:45)
	at test.run(test.groovy:18)
	at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
	at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
	at java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)

If we skip setting includeGenerated to true, the @NullCheck annotation won’t be applied and we will see NullPointerException instead of the IllegalArgumentException.

$ groovy test.groovy
Caught: java.lang.NullPointerException
java.lang.NullPointerException
	at Foo.bar(test.groovy:12)
	at Foo$bar.call(Unknown Source)
	at test.run(test.groovy:18)
	at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
	at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
	at java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)

Thank you!

If you read up to this point - leave me a comment below, so I can thank you in person for such an engagement. You can also share this blog post on Twitter, Facebook, or LinkedIn if you think other people can benefit from reading it. Thanks in advance!

PS: Creating high-quality content takes a lot of time and effort. I want to deliver the best possible value to my readers and keep the blog always ad-free. If you have two more minutes, please take a look at this dedicated page where I listed a few different options to support my efforts. If my vision for the e.printstacktrace.blog development resonates with you, I will be grateful if you consider supporting me in this journey. Thank you!

Szymon Stepniak

27,34396194

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.