Posts How Should A Programmer Deal With Blind Spots
Post
Cancel

How Should A Programmer Deal With Blind Spots

What is a blind spot

A blind spot is an area in your range of vision that you cannot see properly but which you really should be able to see. For example, when you are driving a car, the area just behind your shoulders is often a blind spot.

Statistics from the National Highway Traffic Safety Administration show that nearly 840,000 blind spot accidents occur each year in the United States resulting in 300 fatalities. This is partly due to individuals not adjusting their mirrors properly, or not properly checking their blind spots before changing lanes.

To minimize blind spot related accidents vehicle manufactures are designing blind spot monitor sensors. These sensors alert the driver using visual or audible warning if any vehicles present in the blind spot.

Blind spot in developers life

Just like drivers have to deal with blind spot, developers too have to deal with programming blind spot, we call it the notorious NullPointerException.

To give some historical context, Tony Hoare—one of the giants of computer science—wrote,

“I call it my billion-dollar mistake. It was the invention of the null reference in 1965. I couldn’t resist the temptation to put in a null reference, simply because it was so easy to implement.”

A wise man once said you are not a real Java programmer until you’ve dealt with a null pointer exception. Joking aside, the null reference is the source of many problems because it is often used to denote the absence of a value.

Oracle took some ques from blind spot monitor sensors and introduce a new class called java.util.Optional in Java SE 8 onwards, that can alleviate some of these problems.

Let’s start with an example to see the dangers of null. Let’s consider a nested object structure for a Computer, as illustrated in Figure 1.

Desktop View Figure 1 : A nested structure for representing a Computer

What’s possibly problematic with the following code?

String version = computer.getSoundcard().getUSB().getVersion();

This code looks pretty reasonable. However, many computers (for example, the Raspberry Pi) don’t actually ship with a sound card. So what is the result of getSoundcard()?

A common (bad) practice is to return the null reference to indicate the absence of a sound card. Unfortunately, this means the call to getUSB() will try to return the USB port of a null reference, which will result in a NullPointerException at runtime and stop your program from running further. Imagine if your program was running on a customer’s machine; what would your customer say if the program suddenly failed?

What can you do to prevent unintended null pointer exceptions? You can be defensive and add checks to prevent null dereferences, as shown in the below code.

1
2
3
4
5
6
7
8
9
10
String version = "UNKNOWN";
if(computer != null){
  Soundcard soundcard = computer.getSoundcard();
  if(soundcard != null){
    USB usb = soundcard.getUSB();
    if(usb != null){
      version = usb.getVersion();
    }
  }
}

However, you can see that the code quickly becomes very ugly due to the nested checks. Unfortunately, we need a lot of boilerplate code to make sure we don’t get a NullPointerException. In addition, it’s just annoying that these checks get in the way of the business logic. In fact, they are decreasing the overall readability of our program.

Furthermore, it is an error-prone process; what if you forget to check that one property could be null? I will argue in this article that using null to represent the absence of a value is a wrong approach. What we need is a better way to model the absence and presence of a value.

Let’s look at what Java SE8’s Optional has to offer.

Optional in a Nutshell

Java SE 8 introduces a new class called java.util.Optional. It is a class that encapsulates an optional value, as illustrated in the code below and in Figure 2. You can view Optional as a single-value container that either contains a value or doesn't (it is then said to be "empty"), as illustrated in Figure 2.

Desktop View Figure 2 :An optional sound card

We can update our model to make use of Optional:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class Computer {
  private Optional<Soundcard> soundcard;  
  public Optional<Soundcard> getSoundcard() { ... }
  ...
}

public class Soundcard {
  private Optional<USB> usb;
  public Optional<USB> getUSB() { ... }

}

public class USB{
  public String getVersion(){ ... }
}

The code immediately shows that a computer might or might not have a sound card (the sound card is optional). In addition, a sound card can optionally have a USB port. This is an improvement, because this new model can now reflect clearly whether a given value is allowed to be missing. Note that similar ideas have been available in libraries such as Guava.

But what can you actually do with an Optional<Soundcard> object? After all, you want to get to the USB port’s version number. In a nutshell, the Optional class includes methods to explicitly deal with the cases where a value is present or absent. However, the advantage compared to null references is that the Optional class forces you to think about the case when the value is not present. As a consequence, you can prevent unintended null pointer exceptions.

It is important to note that the intention of the Optional class is not to replace every single null reference. Instead, its purpose is to help design more-comprehensible APIs so that by just reading the signature of a method, you can tell whether you can expect an optional value. This forces you to actively unwrap an Optional to deal with the absence of a value.

Patterns for Adopting Optional

Enough talking; let’s see some code! We will first explore how typical null-check patterns can be rewritten using Optional. By the end of this article, you will understand how to use Optional, as shown below, to rewrite the code in Listing 1 that was doing several nested null checks:

1
2
3
4
String name = computer.flatMap(Computer::getSoundcard)
                          .flatMap(Soundcard::getUSB)
                          .map(USB::getVersion)
                          .orElse("UNKNOWN");

Creating Optional objects

First, how do you create Optional objects? There are several ways:

Here is an empty Optional:

Optional<Soundcard> sc = Optional.empty();

And here is an Optional with a non-null value:

1
2
SoundCard soundcard = new Soundcard();
Optional<Soundcard> sc = Optional.of(soundcard);

If soundcard were null, a NullPointerException would be immediately thrown (rather than getting a latent error once you try to access properties of the ``soundcard`).

Also, by using ofNullable, you can create an Optional object that may hold a null value:

Optional<Soundcard> sc = Optional.ofNullable(soundcard);

If soundcard were null, the resulting Optional object would be empty.

Do Something If a Value Is Present

Now that you have an Optional object, you can access the methods available to explicitly deal with the presence or absence of values. Instead of having to remember to do a null check, as follows:

1
2
3
4
SoundCard soundcard = ...;
if(soundcard != null){
  System.out.println(soundcard);
}

You can use the ifPresent() method, as follows:

1
2
Optional<Soundcard> soundcard = ...;
soundcard.ifPresent(System.out::println);

You no longer need to do an explicit null check; it is enforced by the type system. If the Optional object were empty, nothing would be printed.

You can also use the isPresent() method to find out whether a value is present in an Optional object. In addition, there’s a get() method that returns the value contained in the Optional object, if it is present. Otherwise, it throws a NoSuchElementException. The two methods can be combined, as follows, to prevent exceptions:

1
2
3
if(soundcard.isPresent()){
  System.out.println(soundcard.get());
}

However, this is not the recommended use of Optional (it’s not much of an improvement over nested null checks), and there are more idiomatic alternatives, which we explore below.

Default Values and Actions

A typical pattern is to return a default value if you determine that the result of an operation is null. In general, you can use the ternary operator, as follows, to achieve this:

1
2
3
Soundcard soundcard = 
  maybeSoundcard != null ? maybeSoundcard 
            : new Soundcard("basic_sound_card");

Using an Optional object, you can rewrite this code by using the orElse() method, which provides a default value if Optional is empty:

Soundcard soundcard = maybeSoundcard.orElse(new Soundcard("defaut"));

Similarly, you can use the orElseThrow() method, which instead of providing a default value if Optional is empty, throws an exception:

1
2
Soundcard soundcard = 
  maybeSoundCard.orElseThrow(IllegalStateException::new);

Rejecting Certain Values Using the filter Method

Often you need to call a method on an object and check some property. For example, you might need to check whether the USB port is a particular version. To do this in a safe way, you first need to check whether the reference pointing to a USB object is null and then call the getVersion() method, as follows:

1
2
3
4
USB usb = ...;
if(usb != null && "3.0".equals(usb.getVersion())){
  System.out.println("ok");
}

This pattern can be rewritten using the filter method on an Optional object, as follows:

1
2
3
Optional<USB> maybeUSB = ...;
maybeUSB.filter(usb -> "3.0".equals(usb.getVersion())
                    .ifPresent(() -> System.out.println("ok"));

The filter method takes a predicate as an argument. If a value is present in the Optional object and it matches the predicate, the filter method returns that value; otherwise, it returns an empty Optional object. You might have seen a similar pattern already if you have used the filter method with the Stream interface.

Extracting and Transforming Values Using the map Method

Another common pattern is to extract information from an object. For example, from a Soundcard object, you might want to extract the USB object and then further check whether it is of the correct version. You would typically write the following code:

1
2
3
4
5
6
if(soundcard != null){
  USB usb = soundcard.getUSB();
  if(usb != null && "3.0".equals(usb.getVersion()){
    System.out.println("ok");
  }
}

We can rewrite this pattern of “checking for null and extracting” (here, the Soundcard object) using the map method.

Optional usb = maybeSoundcard.map(Soundcard::getUSB);`

There’s a direct parallel to the map method used with streams. There, you pass a function to the map method, which applies this function to each element of a stream. However, nothing happens if the stream is empty.

The map method of the Optional class does exactly the same: the value contained inside Optional is “transformed” by the function passed as an argument (here, a method reference to extract the USB port), while nothing happens if ``Optional` is empty.

Finally, we can combine the map method with the filter method to reject a USB port whose version is different than 3.0:

1
2
3
maybeSoundcard.map(Soundcard::getUSB)
      .filter(usb -> "3.0".equals(usb.getVersion())
      .ifPresent(() -> System.out.println("ok"));

Awesome; our code is starting to look closer to the problem statement and there are no verbose null checks getting in our way!

References

  • https://docs.oracle.com/javase/8/docs/api/java/util/Optional.html
  • https://www.fortheinjured.com/blog/blind-spot-accident/
  • https://www.oracle.com/technical-resources/articles/java/java8-optional.html
This post is licensed under CC BY 4.0 by the author.