Streams: The Optional Type in Java

An Optional<T> object is a wrapper for either an object of type T or no object. In the former case, we say that the value is present. The Optional<T> type is in­tended as a safer alternative for a reference of type T that either refers to an object or is null. But it is only safer if you use it right. The next three sections shows you how.

1. Getting an Optional Value

The key to using Optional effectively is to use a method that either produces an alternative if the value is not present, or consumes the value only if it is present.

In this section, we look at the first strategy. Often, there is a default that you want to use when there was no match, perhaps the empty string:

String result = optionalString.orElse(“”);

// The wrapped string, or “” if none

You can also invoke code to compute the default:

String result = optionalString.orElseGet(() -> System.getProperty(“myapp.default”));

// The function is only called when needed

Or you can throw an exception if there is no value:

String result = optionalString.orElseThrow(IllegalStateException::new);

// Supply a method that yields an exception object

2. Consuming an Optional Value

In the preceding section, you saw how to produce an alternative if no value is present. The other strategy for working with optional values is to consume the value only if it is present.

The ifPresent method accepts a function. If the optional value exists, it is passed to that function. Otherwise, nothing happens.

optionalValue.ifPresent(v -> Process v);

For example, if you want to add the value to a set if it is present, call

optionalValue.ifPresent(v -> results.add(v));

or simply

optionalValue.ifPresent(results::add);

If you want to take one action if the Optional has a value and another action if it doesn’t, use ifPresentOrElse:

optionalValue.ifPresentOrElse(

v -> System.out.println(“Found ” + v),

() -> logger.warning(“No match”));

3. Pipelining Optional Values

In the preceding sections, you saw how to get a value out of an Optional object. Another useful strategy is to keep the Optional intact. You can transform the value inside an Optional by using the map method:

Optional<String> transformed = optionalString.map(String::toUpperCase);

If optionalString is empty, then transformed is also empty.

Here is another example. We add a result to a list if it is present:

optionalValue.map(results::add);

If optionalValue is empty, nothing happens.

Similarly, you can use the filter method to only consider Optional values that fulfill a certain property before or after transforming it. If the property is not fulfilled, the pipeline yields an empty result:

Optional<String> transformed = optionalString

.filter(s -> s.length() >= 8)

.map(String::toUpperCase);

You can substitute an alternative Optional for an empty Optional with the or method. The alternative is computed lazily.

Optional<String> result = optionalString.or(() -> // Supply an Optional

alternatives.stream().findFirst());

If optionalString has a value, then result is optionalString. If not, the lambda expression is evaluated, and its result is used.

4. How Not to Work with Optional Values

If you don’t use Optional values correctly, you have no benefit over the “something or null” approach of the past.

The get method gets the wrapped element of an Optional value if it exists, or throws a NoSuchElementException if it doesn’t. Therefore,

Optional<T> optionalValue = . . .;

optionalValue.get().someMethod()

is no safer than

T value = . . .;

value.someMethod();

The isPresent method reports whether an Optional<T> object has a value. But

if (optionalValue.isPresent()) optionalValue.get().someMethod();

is no easier than

if (value != null) value.someMethod();

Here are a few more tips for the proper use of the Optional type:

  • A variable of type Optional should never be null.
  • Don’t use fields of type Optional. The cost is an additional object. Inside a class, using null for an absent field is manageable.
  •  Don’t put Optional objects in a set, and don’t use them as keys for a map. Collect the values instead.

5. Creating Optional Values

So    far, we have discussed how to consume an Optional object someone else created. If you want to write a method that creates an Optional object, there are several static methods for that purpose, including Optional.of(result) and Optional.empty(). For example,

public static Optional<Double> inverse(Double x)

{

return x == 0 ? Optional.empty() : Optional.of(1 / x);

}

The ofNullable method is intended as a bridge from possibly null values to op­tional values. Optional.ofNullable(obj) returns Optional.of(obj) if obj is not null and Optional.empty() otherwise.

6. Composing Optional Value Functions with flatMap

Suppose you have a method f yielding an Optional<T>, and the target type T has a method g yielding an Optional<U>. If they were normal methods, you could compose them by calling s.f().g(). But that composition doesn’t work since s.f() has type Optional<T>, not T. Instead, call

Optional<U> result = s.f().flatMap(T::g);

If s.f() is present, then g is applied to it. Otherwise, an empty Optional<U> is returned.

Clearly, you can repeat that process if you have more methods or lambdas that yield Optional values. You can then build a pipeline of steps, simply by chaining calls to flatMap, that will succeed only when all parts do.

For example, consider the safe inverse method of the preceding section. Suppose we also have a safe square root:

public static Optional<Double> squareRoot(Double x)

{

return x < 0 ? Optional.empty() : Optional.of(Math.sqrt(x));

}

Then you can compute the square root of the inverse as

Optional<Double> result = inverse(x).flatMap(MyMath::squareRoot);

or, if you prefer,

Optional<Double> result

= Optional.of(-4.0).flatMap(Demo::inverse).flatMap(Demo::squareRoot);

If either the inverse method or the squareRoot returns Optiona1.empty(), the result is empty.

7. Turning an Optionalinto a Stream

The stream method turns an Optionat<T> into a Stream<T> with zero or one element. Sure, why not, but why would you ever want that?

This becomes useful with methods that return an Optional result. Suppose you have a stream of user IDs and a method

Optionat<User> tookup(String id)

How do you get a stream of users, skipping those IDs that are invalid?

Of course, you can filter out the invalid IDs and then apply get to the remaining ones:

Stream<String> ids = . . .;

Stream<User> users = ids.map(Users::tookup)

.fitter(Optionat::isPresent)

.map(Optionat::get);

But that uses the isPresent and get methods that we warned about. It is more elegant to call

Stream<User> users = ids.map(Users::tookup)

.ftatMap(Optionat::stream);

Each call to stream returns a stream with zero or one element. The ftatMap method combines them all. That means the nonexistent users are simply dropped.

The example program in Listing 1.3 demonstrates the Optional API.

Source: Horstmann Cay S. (2019), Core Java. Volume II – Advanced Features, Pearson; 11th edition.

Leave a Reply

Your email address will not be published. Required fields are marked *