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