Null-Pointer Exceptions Considered Annoying

Posted on by in Development, Java

 

If computer programs have needs, data is one of them. Usually, this data is available, but sometimes it’s not. Why not? Who knows. It’s just not there. What’s worse, this situation is common and, when improperly handled, results in runtime exceptions.

To explore this problem, let’s contrive an example using Java 8. Suppose we want to save a record given some data provided by the user. Since humans are fallible, let’s express the data they provide in terms of the Optional type. Here is what we’ll start with:

Function<String, Integer> saveRecord = m -> {
    System.out.println(String.format("Saving record %s", m));
    // do something useful
    return 0;
};
Optional<String> message = Optional.of("Hello World");
// call saveRecord with the String in message?

What we want to do is call saveRecord if and only if message has a value. Let’s quickly go over the old way of doing this. Later we’ll look at a new way.

The Old Way

In the past, we had to individually test each value we were worried might be null. Leading us to write code like this (but using null and not Optional):

if (message.isPresent()) {
    Integer result = saveRecord.apply(message.get());
}

This works, but it’s tedious because we need to do this same thing all over and it’s error-prone because we occasionally forget. Rather than repeatedly explain to the computer how to correctly work with potentially null data, it would be great if we could describe what we want to happen and let the computer work out how to do it. This declarative approach, the new way of working with optional data, is what the Optional type and its methods provide.

Optional, Declaratively (kind of)

The first method on Optional we’ll look at is map. Here is its type signature:

public <U> Optional<U> map(Function<? super T,? extends U> mapper)

This can be read something like “map takes a function called mapper, which takes data of type T and returns data of type U, it will optionally return data of type U (probably using mapper)”. Specifically, map will only call the mapper function if data is present, otherwise it returns empty. It may not be obvious at first, but, in the context of Optional, map means to get and apply. This can be seen if you compare the previous example with this alternative, which is essentially the same:

Optional result = message.map(saveRecord);

As is, the code above will return Optional<Integer>. This is because map returns an Optional<U> where U is the type of data returned by the mapper function. Since we are using saveRecord as our mapper, U is Integer. What if we didn’t want the result to be optional? We can use orElse. Here is its signature:

public T orElse(T other)

The documentation for orElse says that it will “return the value if present, otherwise return other”. Simple enough. Let’s say that we want to return -1 if at any point our attempt to save a record returns empty. Here is what that might look like:

Integer result = message.map(saveRecord).orElse(-1);

Many Argument Functions

What happens if we add an argument to saveRecord? Well, if you’re not familiar with currying, then the syntax of the following definition of saveRecord may look odd:

Function<String, Function<Float, Integer>> saveRecord = m -> a -> {
  System.out.println(String.format("Saving record %s with amount %f", m, a));
  // do something useful
  return 0;
};

Currying, for the uninitiated, is a technique in which functions over multiple arguments are evaluated as a sequence of functions each of which takes a single argument and returns the next function taking the next argument in the sequence. Though verbose, this can be seen in the type signature of the multiple argument version of saveRecord:

Function<String, Function<Float, Integer>>

Remember that we need to use map to access optional values. Here’s an example given the latest definition of saveRecord:

System.out.println(message.map(saveRecord)); // Optional[Lambda]

You could deduce from the types alone that using map for the second argument of saveRecord will return Optional<Optional<0>>, but it’s easier to just have the computer tell us:

Optional amount = Optional.of((float) 0.3402);
System.out.println(message.map(saveRecord).map(amount::map)); // Optional[Optional[0]]

We get the result we expect, but it’s wrapped in multiple optionals. How did that happen? Where before we knew for sure we had a function we could call, now we don’t. So we have to use another map which introduces another Optional. As you’ve probably already guessed, as we add more arguments this quickly becomes unwieldy. To skirt around this problem we use a combination of map and flatMap. Here is flatMap‘s type signature:

public <U> Optional<U> flatMap(Function<? super T,Optional<U>> mapper)

The only difference between map and flatMap are the return types of their respective mapper functions. Since map’s mapper function does not return an Optional, map needs to take what is returned and wrap it in an Optional. On the other hand, flatMap’s mapper function returns an optional value and so there is no need for flatMap to wrap it in another Optional. Here is the previous example altered to use flatMap:

System.out.println(message.map(saveRecord).flatMap(amount::map)); // Optional[0]

Great! Like before, we can tack on orElse to make sure we always have a value:

System.out.println(message.map(saveRecord).flatMap(amount::map).orElse(-1)); // 0

That’s it. If we have a function we want to call, but we don’t know if we have the data to call it, then we can use this technique to tell the computer what we want to do and let the computer work out how to do it. Admittedly, this doesn’t feel very declarative because we don’t speak in this way, but that can be layered on top of what has been presented here. Java’s Optional type allows programs to be written with fewer null-pointer exceptions. Take advantage of it.

PS I found the Monad image in a tweet from Josh Wills