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.
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)| 1 | Explicit null check in the constructor. |
| 2 | Explicit null check in the method body. |
| 3 | Calling 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.
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. |
| 2 | This 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)




0 Comments