Why I Never Null-Check Parameters
Writing code with input parameters that are not null is not "safer" — it's the opposite.
Join the DZone community and get the full member experience.
Join For FreeWriting code to make sure that input parameters are not null does not make Java code "safer;" it's exactly the opposite — it makes code less readable and less safe.
Code With Null-Checks Is Less Readable
It would be difficult to argue that null-checks are attractive. Most of the time, it's just boilerplate code, which contributes nothing to the "logic" of the method. It is a purely technical construct. Like this one:
@Override
public void getLock(String name,
Handler<AsyncResult<Lock>> handler) {
Objects.requireNonNull(name, "name");
Objects.requireNonNull(handler, "handler");
getLockWithTimeout(name, DEFAULT_LOCK_TIMEOUT, handler);
}
This method, from the otherwise very cool vert.x project, has three lines of code, two of which are checking for the null, so it makes this method somewhat harder to read. These sorts of checks are even more damaging when they actually contribute to the method's logic:
@Override
public Object getValue(final ELContext context,
Object base, Object property) {
BeanManagerImpl beanManager = getManager(context);
if (property != null) {
String propertyString = property.toString();
ElLogger.LOG.propertyLookup(propertyString);
Namespace namespace = null;
if (base == null) {
...
}
This snippet from the popular Weld Project shows how null checks can increase the cyclomatic complexity of the code. In this case, nulls are actually expected and act as flag arguments.
The Reason Null-Checks Exist
The simple reason to check for nulls is the fear of the NullPointerException
, which is ingrained in every Java developer. There is a famous quote from Sir Thomas Hoare, who "invented" the null reference saying:
"I call it my billion-dollar mistake."
Referring to the many crashes, errors, endless hours of debugging, and other resources lost or spent because of null
.
Checking for null parameters, however, is not the correct reaction to this admitted shortcoming of the type-system, because it ignores that there are actually two distinct categories of null-related issues.
The first category is the simple programming error. Errors and bugs can always happen, of course, for example, some parameter somewhere becomes null, even if the code requires a non-null reference. Java already has a mechanism to catch these types of errors by throwing a NullPointerException
. Yes, in these cases, this is not a bad thing. The bug needs to be visible to be easily corrected.
The second category is the problematic one. It is when code deals with null values, often assigning meaning to the null value, like the Weld code with the conditions above. In these cases, there will be no exceptions generated; there will only be some different behavior. It wouldn't be visible if some of the parameters were null by accident, rather, there would be perhaps some business-level issue. Instead of a clear exception to follow, this would probably require debugging or a more in-depth analysis.
The Cost of Null-Checks
Beyond the obvious cost that the additional boiler-plate code introduces, the much greater cost of null-checks is that it legalizes nulls. It makes it OK to pass and receive nulls, thereby increasing the code surface where nulls can cause issues.
It can also hide these issues caused by creating code that can continue to work with a null value in some form, implicitly altering the logic of the code.
Removing Parameter Null-Checks
In a perfect world, it should not be possible to pass or return null values, at least not in public methods. This is one area where Kotlin, Haskell, and others are clearly better designed than Java. We can, however, pretend that nulls don't exist. The question is: is that practical?
In the case of programming errors, it is very easy to find out that nulls were passed on. If there are no null-checks and no alternative paths for execution, it will sooner or later lead to a NullPointerException
. This will be clearly visible, therefore, easy to find and correct.
So the above code:
@Override
public void getLock(String name,
Handler<AsyncResult<Lock>> handler) {
Objects.requireNonNull(name, "name");
Objects.requireNonNull(handler, "handler");
getLockWithTimeout(name, DEFAULT_LOCK_TIMEOUT, handler);
}
Should simply be:
@Override
public void getLock(String name,
Handler<AsyncResult<Lock>> handler) {
getLockWithTimeout(name, DEFAULT_LOCK_TIMEOUT, handler);
}
This strategy, however, does not work if null is a legal value of some parameter or parameters. The simplest solution for these cases is to split the method into multiple ones with each requiring all its parameters. This would transform the Weld code above where the base
parameter is not required:
public Object getValue(final ELContext context,
Object base, Object property) // base not required
To:
public Object getObjectValue(final ELContext context,
Object base, Object property) // base required
...
public Object getRootValue(final ELContext context,
Object property) // base omitted
An alternative solution is to allow the "root" as a base
parameter, in which case the method does not need to be split, and the base
can be a required parameter.
Sometimes, there can be more than 1 or 2 parameters in a signature that are not required. This is often seen in constructors. In these cases, the Builder Pattern might help.
Getting Rid of Null as a Return Value
Removing these checks is only practical if the caller doesn't have to check either. This is only possible if the caller can freely pass in results from other method calls. Therefore, methods shouldn't return null. Every method that returns null legally needs to be changed depending on what meaning this null value carries. Most of the time, it means that a certain object could not be found and the caller should react according to its own logic. In these cases, the Optional
class might help:
@Override
public Optional<Object> getValue(final ELContext context,
Object base, Object property) {
...
return Optional.empty();
}
This way, the possibility of a missing value is explicit, and the caller is free to react however it pleases. Note on Optional
: don't use the get()
method, use map()
, orElse()
, orElseGet()
, ifPresent()
, etc.
Sometimes, returning null means that the caller should execute some default logic defined by the callee. Instead of pushing this responsibility to the caller, the method should return the default logic itself. Let's take a look at an example from Weld again:
protected DisposalMethod<...> resolveDisposalMethod(...) {
Set<DisposalMethod<...>> disposalBeans = ...;
if (disposalBeans.size() == 1) {
return disposalBeans.iterator().next();
} else if (disposalBeans.size() > 1) {
throw ...;
}
return null;
}
One could think of this method returning null to indicate that there is no disposal method defined for a bean instantiated by Weld (a dependency injection framework). But actually, what this method wants to say is that there is nothing to do for disposal. Instead of legalizing null for a lot of intermediary objects, just so that some object can eventually check whether the returned DisposalMethod
is null to do nothing, the method could just return a DisposalMethod
that does nothing. Just have another implementation of it, like:
public final class NoDisposalMethod
implements DisposalMethod {
...
@Override
public void invoke(...) {
// Do nothing
}
}
protected DisposalMethod<...> resolveDisposalMethod(...) {
...
return new NoDisposalMethod();
}
This way, the method does not return null and nobody has to check for null either.
Leftovers
The unfortunate reality is that we can not ignore nulls everywhere. There are a lot of classes, objects, and frameworks over which we have no influence. We have to deal with these. Some of these are in the JDK itself.
For these cases and for these cases only, null-checks are allowed, if only just to package them with Optional.ofNullable()
or check with Objects.requireNonNull()
.
Summary
There is a somewhat prevalent notion that null-checks make Java code safer, but actually, it makes code less safe and less readable.
We should instead pretend that nulls don't exist and treat any occurrence of null as a programming error, at least in public method parameters or return values. Specifically:
- Methods and constructors should not check for nulls
- Methods should not return nulls
Instead:
- Methods and constructors should assume that all parameters are non-null
- Methods that allowed null should be redesigned or split into multiple methods
- Constructors with multiple parameters that aren't required should consider using the Builder Pattern.
- Methods that returned null should return
Optional
or a special implementation of whatever logic was expected on null previously.
Published at DZone with permission of Robert Brautigam. See the original article here.
Opinions expressed by DZone contributors are their own.
Comments