Introduction and motivation
Each Java programmer must have experienced, countless times, a system crash or a similar problem caused by a frustrating NullPointerException
. To protect our software against this problem and make it safer, it is common for us to fill our code with checks that prevent null variables from being called, forcing us to translate a trivial method like this:
public String getProjectManagerName() {
return getProject().getManager().getName();
}
to this:
public String getProjectManagerName() {
Project project = getProject();
if (project == null) return "";
Manager manager = project.getManager();
if (manager == null) return "";
return manager.getName();
}
This approach works fine if you have to use it in just one or two places, but if you need to repeat it across all your software, it dramatically worsens its readability and extensibility by bloating it with unnecessary code. Moreover, this kind of null logic fails to provide protection for new code, so if programmers forget to include it, the same problem can occur again.
This problem is so important to drive in a more modern language like Groovy the introduction of the safe-dereference operator (?.). Using this construct, the former method can be safely rewritten as follows:
public String getProjectManagerName() {
return getProject()?.getManager()?.getName();
}
How the Null Object pattern solves the problem
While the Java language does not provide a safe-dereference operator, it is still possible to achieve the same result by using the so called Null Object pattern. According to Wikipedia:
“A Null Object is an object with defined neutral ("null") behavior.”
In other words, a Null Object of a given class has a type compatible with that class (by extending it or implementing a common interface), and provides a default (“null”) behavior in the absence of a more meaningful one. For example, the Null Object version of the classes Project
and Manager
involved in the former code snippets can be implemented as follows:
public class NullProject extends Project {
public Manager getManager() {
return new NullManager();
}
}
public class NullManager extends Manager {
public String getName() {
return "";
}
}
Note that, of course, a Null Object must always returns another Null Object in order to keep an invocation sequence safe. In this way (provided that the getProject()
method will return an instance of NullProject
when it should return just a null), the first version of our getProjectManagerName()
becomes safe, again eliminating the need for all the checks introduced in the second version.
In my opinion, despite this solution being effective and allowing to write cleaner code, it has a major defect: it obliges us to implement, maintain, and test a Null Object version of each class of our domain model. And again, it doesn’t offer protection for new code in a more subtle way: if a programmer adds a new method to a domain object, forgetting to override it in its Null Object version, you will be exposed to the risk of a NullPointerException
.
Implementing the Null Object pattern with a proxy
In order to remove these last issues, it would be better to dynamically generate the Null Object classes through a proxy instead of statically implementing them. To investigate this last solution, I implemented a class called NullObjectProxy
that is attached to this article, together with a test class that also better illustrates how to use it. The core of this proxy is, of course, in its invoke()
method that defines the actions it performs when an invocation on a Null Object is intercepted.
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
String methodName = method.getName();
if (methodName.equals("getNullClass") &&
(args == null || args.length == 0)) return clazz;
if (methodName.equals("hashCode") &&
(args == null || args.length == 0))
return clazz.hashCode();
if (methodName.equals("equals") && args.length == 1
&& args[0] instanceof NullObject)
return ((NullObject)args[0]).getNullClass() == clazz;
Method mockedMethod = getMockedMethod(method);
if (mockedMethod != null) return mockedMethodValuesMap.get(mockedMethod);
Class<?> returnedType = method.getReturnType();
if (returnedType == Void.TYPE) return null;
if (returnedType.isInterface()) return nullObjectOf(returnedType);
try {
return NullObjectProxy.class.getMethod("null" +
returnedType.getSimpleName() + "Value").invoke(NullObjectProxy.class);
} catch (Exception e) { }
try {
return returnedType.newInstance();
} catch (Exception e) { }
return null;
}
This class allows to dynamically instantiate a proxy that implements the Null Object pattern for any given interface by passing the class of the interface to be mocked to the static constructor:
public static <T> T nullObjectOf(Class<T> clazz);
This proxy guarantees the safety of any invocation sequence because each method call to it returns (when possible) another Null Object, that in turn mocks the type returned by the method itself. In this way, to make safe the original non-checked implementation of the getProjectManagerName()
method, it is enough to return nullObjectOf(Project.class)
from the getProject()
call when it should actually return a null.
To resume, this solution allows to use the Null Object pattern, freeing programmers from the burden of writing a specific Null Object implementation for each of the classes of the domain model. Note that this implementation uses the standard proxy mechanism provided by the Java language, so it currently works only with interfaces, but it is easy to modify it to make it work with any non-final
class by using the cglib library. In each case, you could not implement the Null Object pattern for a final
class since, by definition, you are not allowed to extend it.
As stated, when the declared type returned by the invoked method is not an interface, the Null Object proxy is not able to generate another Null Object. In this case, the proxy tries to return a meaningful value, applying two strategies. First, it checks if there is a predefined default return value compatible with the requested one, and in particular, it returns 0 for any Number
, false for the boolean
, a whitespace for a char
, and an empty String
and a Date
representing the current system time for the String
and the Date
, respectively. Then, if none of the above applies, it tries to instantiate an object of the given return type, but invoking its empty constructor, if any. In the end, if both these strategies fail, the only thing it can do to avoid a ClassCastException
is to return a null.
Despite those default returned values generally being correct in 9 cases out of 10, sometimes, they can be formally wrong. For example, while it makes sense that a Null Object Collection returns 0 when the size()
method is invoked on it, it sounds at least odd that it returns false on a call to its isEmpty()
method. Addressing this last issue has been made possible by override the default behavior for a specific method by calling setMockedMethod()
. For example, to instruct the proxy to return true when isEmpty()
is called on a Null Object Collection, it is sufficient to reconfigure it by calling:
setMockedMethod(Collection.class, "isEmpty", true);