Groovy Cookbook

Groovy 3 @NullCheck annotation - less code and less NPE

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)

Groovy Tutorial | Avoiding NPE with @NullCheck annotation | #groovylang
  • YouTube
  • 5k views
  • 2.5k subscribers

@NullCheck is class, method, or constructor annotation which indicates that each parameter should be checked to ensure it isn't null. If placed at the class level, all explicit methods and constructors will be checked. This feature was added in Groovy 3.0 Watch now »

Did you like this article?

Consider buying me a coffee

0 Comments