Groovy - Simplifying null checks with @NullCheck
While null references have their uses in programming, the creator, Tony Hoare, has admitted that creating null references was a billion-dollar mistake.
If you consider the method parameters, most of the time, it would be a good idea to ensure that the parameter value is not null. Otherwise, we will end up with surprising results such as NullPointerException
or repeated null-checks, making the code hard to understand.
Let's explore what Groovy offers to contain the null values of the method arguments to their boundaries.
Consider the following code.
class Person {
String firstName
String lastName
Person(String firstName, String lastName) {
this.firstName = firstName
this.lastName = lastName
}
String withSalutation(String salutation){
return "${salutation.toUpperCase()}. ${firstName.toUpperCase()} ${lastName.toUpperCase()}"
}
static void main(String[] args) {
def me = new Person("Naresha", "K")
println(me.withSalutation("Mr")) // MR. NARESHA K
}
}
Imagine what would happen if one or more of the firstName
, lastName
, or salutation
is set as null
?
static void main(String[] args) {
def him = new Person(null, null)
println(me.withSalutation(null))
}
You will see the following stacktrace.
Exception in thread "main" java.lang.NullPointerException: Cannot invoke method toUpperCase() on null object
at org.codehaus.groovy.runtime.NullObject.invokeMethod(NullObject.java:91)
at org.codehaus.groovy.runtime.callsite.PogoMetaClassSite.call(PogoMetaClassSite.java:44)
at org.codehaus.groovy.runtime.callsite.CallSiteArray.defaultCall(CallSiteArray.java:47)
at org.codehaus.groovy.runtime.callsite.NullCallSite.call(NullCallSite.java:34)
at org.codehaus.groovy.runtime.callsite.CallSiteArray.defaultCall(CallSiteArray.java:47)
at java_lang_String$toUpperCase.call(Unknown Source)
at com.naresha.demo.nullcheck.Person.withSalutation(Person.groovy:15)
at com.naresha.demo.nullcheck.Person$withSalutation.call(Unknown Source)
at org.codehaus.groovy.runtime.callsite.CallSiteArray.defaultCall(CallSiteArray.java:47)
at org.codehaus.groovy.runtime.callsite.AbstractCallSite.call(AbstractCallSite.java:125)
at org.codehaus.groovy.runtime.callsite.AbstractCallSite.call(AbstractCallSite.java:139)
at com.naresha.demo.nullcheck.Person.main(Person.groovy:22)
If you focus on the exception message - "Cannot invoke method toUpperCase() on null object", it is unclear what variable caused this problem. Of course, in this case, I have intentionally set all three variables to null. In a real-world codebase, it would be accidental. Hence, it would be hard to debug this code. Imagine a codebase of a much bigger size!
The solution is to perform a null check on the arguments of all methods. In this case, the constructor and the method withSalutation
. Doing such null checks with explicit code will result in a cluttered codebase, making it hard to understand its functionality.
Groovy provides the AST transformer groovy.transform.NullCheck
, allowing us to retain null checks' advantage without cluttering the code.
After applying this transformation, the code will look as follows:
import groovy.transform.NullCheck
@NullCheck
class Person {
String firstName
String lastName
Person(String firstName, String lastName) {
this.firstName = firstName
this.lastName = lastName
}
String withSalutation(String salutation) {
return "${salutation.toUpperCase()}. ${firstName.toUpperCase()} ${lastName.toUpperCase()}"
}
static void main(String[] args) {
def him = new Person(null, null)
println(me.withSalutation(null))
}
}
If you run the above program and go through the error message, it mentions, "lastName cannot be null". If you change the lastName value to non-null, you will get the error for firstName and so on.
The AST transformer NullCheck
results in null checks added by the Groovy compiler for every argument of every method. Hence, there are no null checks in the source code but in the compiled code.
You might think it would be a good idea to get one combined exception for all null values - in the above example, one exception for both firstName and lastName is better than the individual exceptions. You are right! But Java doesn't provide a way to return an error along with the return value (go language provides this feature). Hence, Groovy has to rely on exceptions. Also, these null values will be discovered mostly while developing and testing (instead of while users are using the application); it would not impact the user experience. Developers will have to write the tests for null checks for every field.
Let me try to be a devil's advocate (or a smart tester!) by doing the following.
def me = new Person("Naresha", "K")
me.setLastName(null)
println(me.withSalutation("Mr"))
In the above code, since the setLastName
method was generated by the Groovy compiler and not in the original source code, The @NullCheck
transformation was not applied. If you wish to add null checks to the parameters of the generated methods, setting the attribute includeGenerated
to true seems to be the way when you read the documentation.
However, if you try this solution, you will find it does not work. This is because the setter methods are not already generated at the time the AST transformation @NullCheck
is applied. @NullCheck
is applied during the 'Instruction Selection' compilation phase, and the setters are generated during the 'Class Generation' phase which follows the 'Instruction Selection'.
If you add a setter to your Groovy code above (without null checks), the Groovy compiler will apply the @NullCheck
transformation and add the necessary null checks.
// Before transformation
void setLastName(String lastName) {
this.lastName = lastName
}
// After transformation
public void setLastName(lastName) {
if (null == lastName ) {
throw new IllegalArgumentException('lastName cannot be null')
}
this.lastName = lastName
}
Note that the @NullCheck
transformation must not always be applied to the class. It can be applied selectively to the methods, including constructors.
Thus, with a few caveats, the @NullCheck
AST transformation of Groovy is useful for developers to implement concise solutions to introduce null checks.
If are interested in learning about how Groovy handles nulls for collection types check this post.