Module 1 - Optionals

Module Overview

Learn about Java Optionals and how to handle null values effectively to write safer, more maintainable code.

Learning Objectives

  • Implement a method that returns an empty Optional when it has no result
  • Implement a method that wraps its return value in an Optional
  • Implement code to access the wrapped value of an Optional
  • Determine whether a given method should throw an exception, return an empty collection, or return an empty Optional when its result is incalculable, rather than returning null
  • Explain what an Optional is and when to use one
  • Explain the dangers of having a method return null
  • Discuss Optionals as an alternative to returning null objects
  • Outline the different ways to create Optionals and when to use each
  • Outline how to safely access the value in an Optional if it's unknown if the Optional is empty or not
  • Outline how to use the filter, map, and flatMap methods of the Optional class
  • Outline when Optionals shouldn't be used

Optionals

This video introduces the concept of Optionals in Java as a solution to the common issues associated with null values. You'll learn why null references are problematic in Java applications and how the Optional class was designed to provide a more elegant alternative for representing the absence of a value.

Overview

A null reference can be substituted for any object reference. However, it has no type, conveys no information about its reference, and can cause NullPointerExceptions when used. Java does not restrict use of null or require code to handle it. This makes null both dangerous (because it can cause unhandled exceptions at any time) and meaningless (because it can be used without restriction or notice).

Optionals are a popular alternative to null to convey "no object" when that can be a correct outcome, while exceptions are preferred instead of null when an error occurs. Unlike using null, both Optionals and exceptions require developers to account for all possible outcomes.

In this reading, we'll discuss the difficulties NullPointerExceptions cause, as well as alternatives to returning null objects. Optionals aren't used in all situations, so we'll discuss guidelines for when to use them and when to use an "empty" object. We'll show how Optionals should be used as return values in methods. Finally, we'll discuss why Optionals are not intended for use as instance variables or method parameters.

The NullPointerException (NPE)

Methods that return null force their callers to check for the null value or risk throwing a NullPointerException (NPE). Since NPEs are unchecked runtime exceptions, they will propagate until handled or until the program terminates. While an NPE contains a stack trace that shows where code tried to use a null reference and failed, the stack trace does not indicate where the null was assigned. It also doesn't include information about how or why it was assigned, which could include (for example):

  • Failure to connect to a resource such as a database or file
  • A search for records where none met the search criteria
  • All items were filtered out of a collection
  • A calculation was attempted that has no solution

There are many other ways to get a null reference, and each instance is handled differently. For instance, if a method to find text in a file just returns null, the caller cannot assume that the null was caused by an incorrect file name. There could be multiple reasons the call failed. Instead of handling the issue, the caller may simply decide to defer the problem and return null to its own caller, but the farther the issue propagates, the more obscure the original cause becomes.

Since null can be returned in place of any object, NPEs are common and unpredictable. Historically, a common response has been to check for null before using any object. This results in verbose, unreadable, and redundant code. Of course, we should test all these null checks, too, which leads to many test cases that we'd rather avoid.

Alternatives to returning null objects

So, what should we do instead of returning null? It depends. Use the following guidelines to decide which option is correct for the situation.

Alternative 1: If an error occurred, throw an exception

If something goes wrong in your service, you should throw an exception. This could include when you get bad input from your users, or when you get bad responses from services you depend on. If a service you're calling returns a null object when something goes wrong, throwing an exception is better than returning that null object. You've learned about exceptions in previous lessons. Remember that an exception denotes something that "didn't follow the rules". Some rules are more universal than others, however. It should be easy to conclude that attempting to access an unavailable resource (e.g. database) means something didn't follow the rules. But why did that happen? Maybe a service is down, or maybe the credentials are invalid. Both cases warrant exceptions, but probably different ones.

We use checked exceptions to describe error conditions we can anticipate. They force the developer of any calling code to consider and handle those errors. Unchecked exceptions describe error conditions, but do not force calling code to handle them; therefore they can still cause the same error propagation problems that NPEs do. Use checked exceptions as part of a consistent strategy to detect, describe, and handle error conditions.

Alternative 2: Return an empty object if nothing went wrong but there are no results

There are many reasons for returning no results even if nothing went wrong. Maybe you're attempting to retrieve information from a table, and nothing matches the key you sent in. Or you performed a calculation that has no solution for the given parameters. If your processing yields no results for reasons other than error conditions, then return an empty object instead of null.

An empty object is one that has a useful meaning when it contains no other data. The most common empty objects are collections. An empty List, Map, or Set makes intuitive sense. We have some collection of items, but we don't have any items in that collection yet. When you instantiate any of these classes, you usually create them as empty objects, then add to them after. Code that uses, for example, List methods will throw an NPE with a null reference, but will still work as expected with an empty List. Size methods will return 0, for loops will iterate 0 times, et cetera. Returning an empty object indicates "no result" much more clearly and safely than null.

There's a catch, though. Not every object can be empty; it's a design choice. Wrapper classes like Integer and Double can't instantiate without arguments. Classes you write may follow the same pattern, where the class is designed to always have a populated state. There would be no valid empty value. This leads us to...

Alternative 3: If you want to return an empty object, but that type can't be empty, use an Optional

Optional was created as a safer way to indicate "no output from this method, but nothing went wrong". The structure of an Optional class is like other container classes, such as wrappers and collections. Figures 1 and 2 show this similarity.

Figure 1: A memory diagram showing a  variable , with the  referencing a corresponding  object. In Heap memory, the  object has an unpopulated .

Figure 1: A memory diagram showing a String variable empty, with the Stack referencing a corresponding String object. In Heap memory, the String object has an unpopulated value.

Figure 2: A memory diagram showing an  variable , with the Stack referencing a corresponding  object. In Heap memory, the  object has an unpopulated .

Figure 2: A memory diagram showing an Optional<String> variable noValue, with the Stack referencing a corresponding Optional<String> object. In Heap memory, the Optional object has an unpopulated value.

In Figure 2, An Optional<String> called noValue is created using the static method Optional.empty(). At first glance, the resulting memory diagram looks the same as Figure 1. For the String in Figure 1, the key difference is that the instance variable value is a char[], but for the Optional the value can be any object type.

Like on Java collections, there are methods in Optional that encapsulate the value and prevent NPEs if it is not populated. Many of the methods in Optional use generics in either their method parameters or return types. This lets us be strict about what values we store and access in Optionals.

So how do you create and access values in Optionals?

Optional creation

Let's walk through some different ways to make an Optional, including some actions you should avoid.

Optional creation with wrapped values

Let's say you want to create an Integer (remember it can't be empty) and wrap it in an Optional. You can use the static Optional.of(Object) method as shown below.

/**
 * Make an optional with a value.
 * @return optional
 */
public Optional<Integer> makeOptionalWithValue() {
    Integer number = 5;
    Optional<Integer> container = Optional.of(number);
    return container;
}

The returned container is an Optional containing an Integer with a value of 5.

Note that the Optional.of(Object) method parameter can't be null.

/**
 * Attempt to make a wrapped optional without a referenced object.
 * @return optional
 */
public Optional<Integer> makeOptionalWithOutValue() {
    Integer number = null;
    Optional<Integer> container = Optional.of(number);
    return container;
}

This code will result in an NPE, which is just what you're trying to avoid. If you use Optional.of(Object), you must know in advance that the object you're trying to wrap is not null.

Optional creation when you don't need to know if the wrapped value is null or not

Often we use Optionals because we know the value we are wrapping might be null. For this situation, Java provides the Optional.ofNullable(Object) method, which knows how to handle null values. Let's repeat the same two examples above using this method. Here's the first case that shows an Integer with a value of 5.

/**
 * Make an optional with a value.
 * @return optional
 */
public Optional<Integer> makeOptionalWithValue() {
    Integer number = 5;
    Optional<Integer> container = Optional.ofNullable(number);
    return container;
}

The returned container is an Optional containing an Integer with a value of 5.

Here's the second case where the variable number is null.

/**
 * Attempt to make a wrapped optional without a referenced object.
 * @return optional
 */
public Optional<Integer> makeOptionalWithOutValue() {
    Integer number = null;
    Optional<Integer> container = Optional.ofNullable(number);
    return container;
}

In this case, Optional.ofNullable() sees that number is null and returns an empty Optional. Note that we don't have to provide null checks; ofNullable does that for us.

Because the method return value is Optional<Integer>, callers know that this method may or may not return a value. We're also implicitly promising that we'll never return null, even though the return is an Optional object and therefore could be represented with a null reference.

Using Optionals

How can you safely access the value in an Optional once you make one? There are a couple of ways, the one you choose depends on what you're trying to accomplish and whether you know if the Optional is empty or not.

Optional.isPresent() and Optional.get()

The example below returns a String message about the number of items in a shopper's cart at checkout. It uses helper method getNumberOfItemsInCart(), which returns an Optional<Integer>. isPresent() is used to see if there are items in the cart. If there are items, get() is used to retrieve the value stored in the Optional and populate the message. If the cart is empty, a different message is returned. Note that without the isPresent() check, if the cart was empty then get() would throw a NoSuchElementException. We need the isPresent() check to branch and handle the value correctly.

/**
 * Return a message to shoppers about the number of items in cart as they checkout.
 * @return String checkout message
 */
public String checkoutMessage() {
    Optional<Integer> numberOfItems = getNumberOfItemsInCart();
    if (numberOfItems.isPresent()) {
        return "There are " + numberOfItems.get() + " items in your cart."; 
    }
    return "There were no items in your cart. Would you like to return to the store?";
}

This looks a lot like a regular null check. However, if getNumberOfItemsInCart returned null when there were no items in the cart, we wouldn't know until we checked its source code or received a NPE. Because it returns an Optional, we know we have to check whether a value was returned.

Java streams and lambdas also provide us with a more compact, more meaningful way to implement this method, which we'll describe after we learn a few more methods on Optional.

Optional.orElse()

If you know what default value to use when an Optional is empty, you can use orElse(T default). This method will retrieve the value stored in the Optional, or return the default you specify if the Optional is empty. This is useful when you know what value to use if no value currently exists, and if the rest of your calculations would continue the same once a default value was set.

Below, we have a method to determine what treat to get for a user on their birthday. An Optional<Boolean> is retrieved from helper method userLikesCake(User). We are conscious of people's dietary choices, so if we don't specifically know they like cake, we default to false for the rest of our calculations. This way, whether we know a user's preferences or not, we can write code that handles each case without needing 3 different branches.

/**
 * Return the treat the user would like for their birthday.
 * @param user - The User to look up preferences for.
 * @return "cake" if we're certain the user likes cake, or "fruit basket" otherwise.
 */
public String birthdayTreat(User user) {
    Optional<Boolean> likesCake = userLikesCake(user);
    if (likesCake.orElse(false)) {
        return "cake";
    } else {
        return "fruit basket";
    } 
}

In practice, we would use the result of userLikesCake() immediately instead of storing the result in a variable. Combined with the ternary operator, we can reduce the entire method to a single line!

/**
 * Return the treat the user would like for their birthday.
 * @param user - The User to look up preferences for.
 * @return "cake" if we're certain the user likes cake, or "fruit basket" otherwise.
 */
public String birthdayTreat(User user) {
    return userLikesCake(user).orElse(false) ? "cake" : "fruit basket";
}

Optionals, Streams, and Lambdas

Optional provides stream-like operations, including filter, map, and flatMap. These methods "pass through" empty Optionals, allowing us to rewrite the previously described methods like this:

public String checkoutMessage() {
    return getNumberOfItemsInCart()
        .map(numItems -> "There are " + numItems + " items in your cart.")
        .orElse("There were no items in your cart. Would you like to return to the store?");
}

The map method will simply pass through an empty Optional, or it will execute the lambda and wrap the result in an Optional if it has a value. Then orElse unwraps the newly-mapped value, if it exists, or returns our default value if the Optional was empty. This has the same effect as the if-else statement used earlier.

This can have much greater effect when replacing chained references. For instance, consider this Java statement finding the name of a person's mortgage company:

return person.getHouse().getMortgageCompany().getName();

Not every person lives in a house. Even if we happen to be looking at a person who lives in a house, they may not have a mortgage company. At least we can be certain that every mortgage company has a name. If these methods return null when no such object exists, this line could raise NPEs in at least two places. To protect ourselves, we must write many null checks:

House house = person.getHouse();
if (house != null) {
    Company mortgageCompany = house.getMortgageCompany();
    if (mortgageCompany != null) {
        return mortgageCompany.getName();
    }
}
return "UNKNOWN";

On the other hand, if getHouse and getMortgageCompany return Optionals, we can write this:

    return person.getHouse()
        .flatMap(house -> house.getMortgageCompany())
        .map(company -> company.getName())
        .orElse("UNKNOWN");

The flatMap is used because getMortgageCompany() already returns an Optional; if we used map, it would wrap the result in another Optional. flatMap flattens the result into a single Optional.

Optional can make long chains of method calls safe and understandable.

Optionals and exceptions in action

Next, let's look at some code that interacts with services that return null, but that avoids returning null itself. The entry point is getFormattedBalance(String). It displays a string for the user's balance. It retrieves the balance from accessBalance(String), which handles exceptions and returns an Optional<BigDecimal>.

Unfortunately, accessBalance must deal with a BalanceFinder service that returns a BigDecimal, and uses null to indicate that it couldn't find a balance for the user. Luckily it does some exception handling, so accessBalance can tell the difference between an invalid user name and a user without a balance.

/**
  * Look up the 401K balance of a person by name.
  * @param name The name of the person to find a balance for.
  * @return an Optional BigDecimal, representing the person's 401K balance, if any.
  * @throws IllegalNameException if the name is not valid.
  */
public Optional<BigDecimal> accessBalance(String name) throws IllegalNameException {
    // We still protect our code from null, but we use specific checked exceptions
    if (name == null) {
        throw new IllegalNameException("Name must not be null!");
    }

    // BalanceFinder will throw IllegalArgumentException if the name is invalid (all numbers,
    // for instance). We convert that to a checked exception. It also returns null if
    // it can't find a 401K balance for the name, which we convert to an Optional.
    try {
        return Optional.ofNullable(BalanceFinder.lookUpBalance(name);
    } catch (IllegalArgumentException e) {
        logger.info("BalanceFinder reported invalid name!", e);
        throw new IllegalNameException("Invalid name!", e);
    }
}

/**
 * Format a person's 401K balance, or use some default text.
 * @param name The name of the person who needs a formatted 401K balance.
 * @returns A formatted String. Never null.
 */
public String getFormattedBalance(String name) {
    // We still protect our code from nulls.
    if (name == null) {
        return ("Error: Name can't be null.  Reenter name.";
    }

    try {
        return this.accessBalance(name)
            .map(balance -> name + "'s balance is $" + balance.toString())
            .orElse(name + "'s balance can't be located");
    } catch (IllegalNameException e) {
        return "Error: Name [" + name + "] has wrong format.";
    }
}

Why shouldn't Optionals be used for all class/method variable types?

Remember, Optionals should be used as return values. Here's a few situations where you specifically don't want to use Optionals:

  • As method parameters: If you do this, you force the caller to handle whether the parameter might be null. It's usually unnecessary and requires a common understanding of the null processing rules across the two methods. A year from now, someone (including you) may forget why the null processing in the other method was needed and remove it, leading to errors.
  • As instance variables: This leads to verbose and cumbersome setters and getters. Instance variables are an appropriate place to use null references, where setters and getters can do validation checking and maintain the object's state.
  • As getters: Determining whether a getter can return null is part of the object's design and business rules. In general, getters are expected to return a direct representation of their values (possibly with some defensive copying).

Summary

Returning null objects in methods is a risky practice. It's been causing NPEs in code for many years with lots of associated pain and wasted effort. On top of crashing your code, NPEs provide very little information about the source of the problem, which makes it difficult and expensive to debug and fix them. Follow these helpful guidelines to use alternatives and avoid returning null objects.

  • If an error has occurred, throw an exception (deciding if it's an error is often the hardest part, yourteam and TPM can help with that)
  • If it's not an error but no results can be returned, return an empty object
  • If the object type you're returning can be empty, return an empty instance of that type, don't wrap it.
  • If the type can't be empty, wrap it in an Optional.

Java provides several static methods for creating Optionals (we discussed empty(), of() and ofNullable()). Java also provides several instance methods for safely accessing wrapped Optional values (we discussed isPresent(), get() and orElse()). By using these, you can avoid nulls and their associated issues.

Guided Project

This deep dive into Java Optionals covers practical examples of creating, using, and manipulating Optional objects. The video demonstrates common patterns and best practices that help you leverage Optionals effectively in your code to handle nullable values safely.

Resources