Using Optional to Transform Your Java Code

Using Optional to Transform Your Java Code image

Java doesn’t have pointers. So why does it have the “Null Pointer Exception?”

Because Java really does have pointers: they’re “references.” References give us some of the power that pointers in C and C++ give us, such as passing large objects between modules without having to copy them, but at the same time not allowing some of the more common mistakes, such as using a pointer to an object X as a pointer to object Y.

But it’s still possible to try to use a reference that doesn’t point to anything, i.e., a null reference. This generates the curiously named Null Pointer Exception.

To help avoid these exceptions, Java 8 introduced the Optional<> class. Optional is a parameterized class that encapsulates a value that may or may not be there. Optional types have existed in functional languages for a long time, and when Java 8 introduced many functional features, adding in Optional made much sense.

Optional is also a great “gateway” class to using Java 8’s functional features. It accepts function objects, including lambdas of course, which can make code more concise and easier to read.

Let’s start with a typical use-case for null references and examine a few different ways to use Optional as a replacement, and see how adding it to your repertoire can change the way you code. You can also find the sample code for this post over here in GitHub.

Without Optional

Let’s consider a pretty common problem in Java: retrieving values from a HashMap.

public static void main(String[] args) throws Exception {

    HashMap<Integer, String> stringMap = new HashMap<>();
    stringMap.put(0, "John Doe");
    stringMap.put(1, "Alfred Neuman");
    stringMap.put(2, "John Galt");

    for (int i = 0; i < 4; i++) {
        String name = stringMap.get(i);
        if (name != null) {
            System.out.println("Name " + i + " is " + name);
        } else {
            System.out.println("Name " + i + " is not found.");
        }
    }
}

We place three values into a HashMap and then try to retrieve four. This is obviously a contrived case. We are using the fourth access as a replacement for incorrect user input or some other situation that results in a key that does not have a value.

If HashMap is passed a key that does not exist, it returns a null reference. Checking the result of retrieving something from a HashMap is a precaution all Java programmers are accustomed to.

When we run this code we see:

Name 0 is John Doe
Name 1 is Alfred Neuman 
Name 2 is John Galt 
Name 3 is not found

Optional.isPresent()

Let’s start with the most basic use of Optional: using the isPresent() method to check if it is empty.

public static void main(String[] args) {

    HashMap<Integer, String> stringMap = new HashMap<>();
    stringMap.put(0, "John Doe");
    stringMap.put(1, "Alfred Neuman");
    stringMap.put(2, "John Galt");

    for (int i = 0; i < 4; i++) {

        Optional optName = Optional.ofNullable(stringMap.get(i));

        if (optName.isPresent()) {
            System.out.println("Name " + i + " is " + optName.get());
        } else {
            System.out.println("Name " + i + " is not found.");
        }
    }
}

This produces the same output as above.

We wrapped the call to stringMap.get() in an Optional.ofNullable(), which creates an Optional that contains the result of the get().

There are three static methods for creating a new instance:

  • Optional.of() is for wrapping something that is not null
  • Optional.ofNullable() is for something that may be null, such as a method that may return null, like the example above.
  • Optional.empty() is for a creating an Optional that contains nothing.

We then call isPresent() to see if the Optional contains a value. This simply replaces the null check with a call to a boolean method. In terms of increasing readability or safety, this use of Optional does little. We still have that if/else block, and it’s still possible to try to use the reference without making sure it’s there. You can modify the code to call get() without the check. It will compile and then throw a NoSuchElementException. We exchanged one exception for another.

Before we move on, let’s create an object that returns an Optional.

public class UserDictionary {

    final HashMap<Integer, String> theList = new HashMap<>();
    
    UserDictionary() {
        theList.put(0, "John Doe");
        theList.put(1, "Alfred Neuman");
        theList.put(2, "John Galt");
    }
    
    void addUser(int number, String name) {
        theList.put(number, name);
    }

    Optional getUserByNumber(int number) {
        return Optional.ofNullable(theList.get(number));
    }
}

Wrapping utility classes like HashMap in an Optional makes sense, but an API that returns an Optional instead of references is even better. I maintain an API at my day job, and I’ve moved to that model. You’ll see why very soon.

Here’s our sample code now. It’s a little cleaner, but only because we moved the HashMap initialization out of sight. IsPresent() is still just a replacement for != null.

UserDictionary userDictionary = new UserDictionary();

for (int i = 0; i < 4; i++) {
    Optional optName = userDictionary.getUserByNumber(i);
    if (optName.isPresent()) {
        System.out.println("Name " + i + " is " + optName.get());
    } else {
        System.out.println("Name " + i + " is not found.");
    }
}

Optional.ifPresent()

IfPresent() gives us a way to execute code if and only if the Optional contains something.

static void main(String[] args) {

    UserDictionary userDictionary = new UserDictionary();

    for (i = 0; i < 4; i++) {         
      userDictionary.getUserByNumber(i)
      .ifPresent(name -> System.out.println("Name " + i + " is " + name));
    }
}

This method accepts a Consumer, a functional interface with a method that is executed if the item is there. For this initial example we supplied a lambda expression. We also had to declare the loop variable at file scope so it could be used in the lambda. (Which is terrible and is only done here to illustrate a point.) Lambdas are a topic I’ll go into in a future post. For now, it’s enough to understand this:

item -> action;

Where the item is the contents of the Optional and action is one or more statements that can only access the item and “effectively” final variables. Our initialization of i outside the scope of the for loop satisfies that.

A lambda is not limited to a single statement. For multiple statements, use curly braces.

userDictionary.getUserByNumber(2).ifPresent(name -> 
   {
      name = name.concat(" is a nice guy");
      System.out.println(name);
   });

This prints “John Galt is a nice guy” to the terminal. Name is a local variable and goes out of scope when the lambda completes. The value inside the dictionary is not, of course, modified by the action.

Let’s implement a Consumer.

class ConsumeName {
    void printAName(String name) {
        System.out.println("Name is " + name);
        name = name.concat(" is a nice guy");
        System.out.println(name);
    }
}

This is the same code as our lambda, but over in its own object.

public static void main(String[] args) {
    ConsumeName consumer = new ConsumeName();
    UserDictionary userDictionary = new UserDictionary();
    for (int i = 0; i < 3; i++) {
        userDictionary.getUserByNumber(i).ifPresent(consumer::printAName);
    }
}

Before we start the loop we create a ConsumeName and then pass it to each Optional as we go.

This gives us a very powerful way to do something when the Optional contains an object, and eliminates the check to see if **Optional **contains a value. But what if we want to take action when it doesn’t contain anything, rather than simply skipping over it?

Optional.orElse()

public static void main(String[] args) {

    UserDictionary userDictionary = new UserDictionary();

    for (int i = 0; i < 4; i++) {
        String user = userDictionary.getUserByNumber(i).orElse("not found");
        System.out.println("Name " + i + " is " + user);
    }
}

The orElse() method does exactly what it says: return the contents or else return the value we’ve passed in. This produces the same output as our first example.

Name 0 is John Doe 
Name 1 is Alfred Neuman 
Name 2 is John Galt 
Name 3 is not found

We replaced the six lines of code in our first loop with only two.

Optional.orElseGet()

OrElse() satisfies the need for replacing a missing value with a what is effectively a constant value, but what if want to create the replacement dynamically? For that we have orElseGet(), which accepts a Supplier.

public static void main(String[] args) {

    UserDictionary userDictionary = new UserDictionary();

    for (int i = 0; i < 4; i++) {
        String user = userDictionary.getUserByNumber(i)
        .orElseGet(OptionalOrElseGet::produceName);
        System.out.println("Name " + i + " is " + user);
    }
}

static String produceName() {
    return "empty name";
}

A Supplier is simply a method that accepts no arguments and returns the required type.

Similar to a Consumer, what’s important in creating a Supplier is the type. A Consumer must accept the right type and a Supplier needs to return the correct type. In this case we’re using a static method for our Supplier, since we are running from main(), which is always static. Suppliers do not have to be static.

While the requirement to not accept any arguments does place some constraints on Suppliers, this mechanism gives us a way to dynamically create a replacement for an empty Optional.

We can use a lambda, too.

public static void main(String[] args) {

    UserDictionary userDictionary = new UserDictionary();

    for (int i = 0; i < 4; i++) {         
        String user = userDictionary.getUserByNumber(i).orElseGet( () -> 
        {
            System.out.println("Name not found!");
            return "empty name";
        });
        System.out.println("Name " + i + " is " + user);
    }
}

In the case of a lambda that has no item (since we only call this if there is nothing to pass to the lambda), use empty parentheses in place of the item.

Optional.orElseThrow()

Sometimes an empty Optional is a fatal error.

public static void main(String[] args) throws Exception {

    UserDictionary userDictionary = new UserDictionary();

    for (int i = 0; i < 4; i++) {         
        String user = userDictionary.getUserByNumber(i)
        .orElseThrow(() -> new Exception("Name not found!"));
        System.err.println("Name " + i + " is " + user);
    }
}

We can throw an exception if the Optional is empty. OrElseThrow() accepts a Supplier that is expected to return an Exception, rather than a constructor, since the Exception can be created lazily. Above we used a lambda that construct a new Exception.

In the API at my day job, and there are a few circumstances in the API where I have wrapped the acquisition of a critical resource in an Optional and need to throw if acquiring it fails. Rather than using a lambda, I created a new class that extends our internal exception class, similar to this:

public class NameNotFoundException extends Exception {

    NameNotFoundException() {
        super("Name not found!");
    }
}

So I can do something like this:

String user = userDictionary.getUserByNumber(i).orElseThrow(NameNotFoundException::new);

What used to be the typical five or six line if/else pattern is now a single line that quickly conveys what is happening.

Optional.filter() and Optional.map()

Last, Optional gives us a way to transform and filter items as they are retrieved.

public static void main(String[] args) { 
   UserDictionary userDictionary = new UserDictionary(); 
   for (int i = 0; i < 3; i++) { 
       userDictionary.getUserByNumber(i) 
       .map(String::toUpperCase) 
       .ifPresent(name -> System.out.println("Name is " + name)); 
    } 
}

Map() accepts a Function object that will be applied to the item if it is there. This example produces:

Name is JOHN DOE 
Name is ALFRED NEUMAN 
Name is JOHN GALT

Function is another interface introduced with lambdas. It simply needs to be a member function of the type enclosed by the Optional, and cannot accept any parameters. For our example String’s toUpperCase() is perfect.

We then called ifPresent() on the result of map(). We can do this because map() returns a new Optional. This allows us to chain calls to Optional, so we added an ifPresent() to print our result.

There’s an interesting side effect lurking in there: we can have our map() function return null if we wish, and the next method simply won’t be called. (Unless it’s an orElse() or an orElseThrow(), of course.)

Having map() return a null sounds a lot like filtering through. So instead of relying on side effects, we have a method for filtering.

public static void main(String[] args) {

    UserDictionary userDictionary = new UserDictionary();
    for (int i = 0; i < 3; i++) {         
        userDictionary.getUserByNumber(i)                 
        .map(String::toUpperCase)                 
        .filter(item -> item.charAt(0) != 'J')
        .ifPresent(name -> System.out.println("Name is " + name));
    }
}

Filter() accepts a Predicate, another functional interface. In the example above, we simply supplied a lambda function that accepts our type, String, and returns true or false based on the first character. This filter removed “JOHN DOE” and “JOHN GALT” so we only see “ALFRED NEUMAN” in our output.

Conclusion

Optional helps us protect ourselves from null pointer exceptions, while at the same time helping us write cleaner and more declarative code. Sample code that corresponds to each of the uses of Optional I demonstrated here can be found in my GitHub repository here.

I bet if you try introducing it into a few projects you find it changes how you think and how you write code.

KEEP MOVING FORWARD

Eric Goebelbecker / code