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.
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
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
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