Estimated reading time: 11 minute(s)
"I call it my billion-dollar mistake. It was the invention of the null reference in 1965. At that time, I was designing the first comprehensive type system for references in an object oriented language (ALGOL W). My goal was to ensure that all use of references should be absolutely safe, with checking performed automatically by the compiler. But I couldn't resist the temptation to put in a null reference, simply because it was so easy to implement. This has led to innumerable errors, vulnerabilities, and system crashes, which have probably caused a billion dollars of pain and damage in the last forty years." Tony Hoare
Object references in code don't tell the developer that a reference is null. The compiler, your best tool for code integrity, doesn't have a way of knowing when a reference is null or not. Both the person writing the code and the software to verify the code can't handle this problem. This inevitably causes these programming errors to be pushed off until runtime as NullPointerExceptions. It's not uncommon for these errors to occur after the code has been deployed to production and it fails for the customer. Nulls are dangerous and there isn't anything to ensure they are handled properly.
The Safe Navigation Operator is simply syntactic sugar that aimed to avoid NullPointerExceptions. It is not a solution for null. It simply helps cover up the fact that null is there, and makes writing code around nulls easier and cleaner. For example:
// This is sudo code
final String accountNumber = account?.getAccountNumber();
This isn't any different than writing the following code:
final String accountNumber;
if (account == null) {
accountNumber = null;
} else {
accountNumber = account.getAccountNumber();
}
Some languages do however have help with the compiler to help the accuracy of this operator. This generally isn't perfect and still is only tackling the issue of NullPointerException avoidance. This is only part of the issue with nulls.
Let's say that when you walk into a room in your house, you may or may not get sprayed in the face with water. However, if you turn on the light before you enter into a room, it's guaranteed that you won't be sprayed in the face with water. How long will it take until you turn on the light before entering a room, whether you need to or not just to avoid getting sprayed in the face? This is exactly what happens with nulls. How many times do developers have to get bitten by nulls before they take a brute force approach and start null checking everything? What new problems are they going to introduce with this approach, and what will they suppress that wasn't intended?
Developers who work in a code base that uses nulls will never fully understand the data they are working with. They will be too focused on avoiding NullPointerExceptions and won't be focused on understanding the data. Take the following method signature:
public User getUser()
What is the developer going to think when they call this method? When they call this method, are they going to write the following code?
final User user = getUser();
if (user != null) {
// do something
}
What if getUser() comes back as null and someone forgets to handle it? This is a common and easy mistake. What if this method will never return a null User? How many pointless null checks does this introduce in the system due to paranoia of null references? With pointless null checks, you now have code falsely documenting "In our business domain, getUser() can be null because other developers are null checking it" instead of, "Why is getUser() returning null?" This is a problem if it is null. Who broke the code? How could you ever understand the data when you are too fixated on handling null?
Nulls have the potential of introducing false positives. Since developers don't want code throwing NullPointerExceptions, they are more likely to write code that is null protected. This can be a bad mindset and they'll suppress code doing so. For example:
public void send() {
if (emailAddress != null) {
sendEmail(emailAddress);
}
}
If emailAddress is null, nothing will happen even though the developer called send() on this object. Time is now required to figure out why send() isn't working. You could argue that this code is really suppressing an error and is no different than the following code.
public void send() {
try {
sendEmail(emailAddress);
} catch (final NullPointerException e) {
}
}
The symptom is a NullPointerException, but what is the cause? The solution may seem to be null checking emailAddress, but maybe the cause is it being called from the wrong layer. Maybe something wasn't loaded correctly, or there is a bug in the user validation. There are a lot of possibilities depending on context. It would be better to write the code above as:
public void send() {
if (emailAddress == null) {
throw new RuntimeException("[Insert descriptive explanation and potential fixes]")
} else {
sendEmail(emailAddress);
}
}
This helps prevent false positives and saves the developer and/or user time by informing them of the problem.
Ideally, handling nulls should be done with the compiler. A lot of times, problems are pushed to runtime. By creating a type that represents a nullable object, you can push nulls to the closest thing to a "compile time" check. This pattern is referred to as the Option Pattern. In Java, this is implemented with the object Optional. Here is an example of it:
final Optional<Contact> contact = contactDao.find(id);
if (contact.isPresent()) {
sendMessage(contact.get());
} else {
showContactNotFoundMessage();
}
isPresent() becomes your null check. get() is how you actually get the value to use: in this case a Contact.
In Java, the Optional class's Javadoc states that it was intended for method returns only. In other languages, the Option Pattern is the replacement for null. Software is expected to be fully expressed with very few types, and there isn't a type for null. Software developed with few types increases bugs, development time, and the amount of unit tests that need to be written. Use the Option Pattern for a value that is nullable. In the case of parameters in methods or constructors, try using method overloading or default values, if your language supports it. If you ultimately need a null value, don't use null, use an optional. This would apply for fields as well. Don't use null!
Standardize your code base with the following:
// Method signature states user will never be null
public User getUser();
// Method signature states user will be optional and needs to be handled
public Optional<User> getUser();
Now there isn't a question of if something is null anymore.
When you are calling a method that guarantees an object and that object can't be provided, throw an exception. This should be viewed as a bug in the application and not something that simply needs to be null checked. For example:
try {
final User user = userDao.findSuperUser();
// ...
} catch (final MissingSuperUserException e) {
// handle exception
}
This is something that is expected, but not there. An exception is perfect for this use case.
When working with collections, it never makes sense to use null. You either have a collection with items, or you have an empty collection. If you are in a situation where null feels like what you need to use, replace it with an empty collection. This same thing would apply to Strings. Use an empty string instead of null.
The Null Object Pattern is another way to deal with null. This pattern via polymorphism defines a "null" object; meaning an object that doesn't do anything but stand in for null. This allows the code to execute without any null checks. For example:
if (cat == null) {
throw new RuntimeException("Cat object cannot be null.");
} else {
cat.meow();
}
With the Null Object Pattern, a type is defined to represent null. For example:
public interface Cat {
void meow();
}
public final class Tiger implements Cat {
@Override
public void meow() {
System.out.println("ROAR!");
}
}
public final class NullCat implements Cat {
@Override
public void meow() {
}
}
Now in the first example, you can remove the null check completely. You will either have an instance of Cat or a NullCat.
When working with legacy code/APIs that use nulls, you'll want to look for a newer replacement that doesn't support these bad practices. If you can't avoid these bad practices in the API, wrap the code so only in your wrapper class(es) will you have to deal with it.
If you are working in legacy code, you may not be able to implement these standards. One thing that you can do to ease the pain of working with nulls is to add the suffix "OrNull" to your methods and variable names. For example:
final Contact contactOrNull = contactDao.findOrNull(name);
It isn't the ideal approach mentioned above, but it is better than not having anything there to tell the developer.
Nulls are great in unit tests. Nulls document "this value is irrelevant for what is being tested." Nulls are used in method and constructor parameters that don't apply to what is being tested. For example:
public final class Car {
private final Engine engine;
private final Transmission transmission;
public Car(final Engine engine, final Transmission transmission) {
if (engine == null) {
throw new RuntimeException("Engine is required");
}
if (transmission == null) {
throw new RuntimeException("Transmission is required");
}
this.engine = engine;
this.transmission = transmission;
}
}
Now we want to test that when a car is instantiated, the doors default to locked.
@Test
public void defaultsTheDoorsToLocked() {
final Engine engine = new Engine();
final Transmission transmission = new Transmission();
final Car car = new Car(engine, transmission);
assertThat(car.isAllDoorsLocked(), is(true));
}
The null checks make this more difficult to test. You must provide both an Engine and a Transmission, but neither have anything to do with the doors of the car. If you are not applying the principles above, you won't be able to utilize the power of null in your tests. The idea is to not check for null because it isn't allowed in your production code. If the production code null checks were gone, you could clean up your unit tests. The following example is with the null checks removed from car.
public final class Car {
private final Engine engine;
private final Transmission transmission;
public Car(final Engine engine, final Transmission transmission) {
this.engine = engine;
this.transmission = transmission;
}
}
Now you can write your unit test as expected, and without any pointless objects needed.
@Test
public void defaultsDoorsToLocked() {
final Car car = new Car(null, null);
assertThat(car.isAllDoorsLocked(), is(true));
}
You don't need an Engine or a Transmission to check if the doors are locked. Those dependencies have nothing to do with the car doors. This test is easier to write, easier to read, and gets right to the point.
Following the standards discussed here, you can avoid the "billion-dollar mistake" by eliminating the use, and misuse, of nulls. By using proper types to indicate and handle null references, self-documenting naming conventions, and method signatures, nulls can be eliminated from your code base.
Author: Crono
,