11.5
Optional, and the Habit It Is Trying to Break
A method that might not find anything has to say so somehow. Returning null says it in a way the compiler cannot see, and that is the whole problem.
Previously on
Four operations in Section 11.4 gave you something you have not been introduced to:
staff.stream().min(bySalary); // Optional<Emp>
staff.stream().findFirst(); // Optional<Emp>
staff.stream().reduce(Integer::sum); // Optional<Integer>None of them returned null, and none of them could promise a value either. This section is about what they returned instead, and the reason it exists is older than streams.
The problem
Write a method that looks something up and might not find it.
Student findByRoll(int roll) {
for (Student s : students) {
if (s.rollNumber() == roll) return s;
}
return null;
}null is the only honest answer available. There is no Student to return.
Now look at what the caller sees. The signature says Student findByRoll(int roll). It promises a Student. Nothing in it mentions null, so this compiles:
String name = findByRoll(999).name();It compiles, it ships, and it throws NullPointerException when roll 999 is not on the list. The compiler had no way to warn you, because as far as the type system is concerned the method returns a Student and Student has a name() method.
Documentation does not fix it. You can write @return the student, or null if not found in a comment, and the next person will not read it. The type is what gets read, and the type is lying.
Checking everywhere is not much better.
Student s = findByRoll(101);
if (s != null) {
Address a = s.address();
if (a != null) {
City c = a.city();
if (c != null) {
System.out.println(c.name());
}
}
}Four levels of indentation to print one name. Each if is there because a method somewhere might return null and did not say so. Miss one and you have the same crash back, in a place further from the cause.
The idea
Change what the method promises.
Optional<Student> findByRoll(int roll) { ... }Now the signature says what is true. There might be a student. There might not. The caller cannot get at the value without dealing with both cases, because there is no name() method on an Optional.
findByRoll(999).name(); // does not compileThat failure is the entire point. The mistake moved from run time to compile time.
Making one:
Optional.of(student) // I have a value. Throws NPE if it is null
Optional.ofNullable(maybe) // might be null, sort it out for me
Optional.empty() // definitely nothingof throwing on null looks unhelpful and is deliberate. Optional.of(x) is you telling Java there is a value. If there is not, you were wrong, and finding out immediately beats carrying an empty Optional you thought was full.
Opening it, and the wrong way first:
if (result.isPresent()) {
use(result.get());
}That works and it is a waste. You have swapped != null for isPresent() and kept every problem: two branches to write, and the possibility of skipping the check and calling get() anyway.
The right way is to not open it at all. Say what should happen and let the Optional apply it:
| Method | What it does | |
|---|---|---|
| orElse(other) | a value, or this one | the fallback is built either way |
| orElseGet(supplier) | a value, or call this | the fallback is built only if needed |
| orElseThrow(supplier) | a value, or throw | when empty really is an error |
| ifPresent(consumer) | do this if there is one | nothing at all if there is not |
| ifPresentOrElse(c, r) | do this, or that | both branches, no if |
| map(function) | change what is inside | stays empty if it was empty |
| filter(predicate) | keep it only if it passes | becomes empty if it fails |
map and filter are the ones that change how the code reads. They are the same two methods from streams, doing the same job on a container that holds nothing or one thing.
The four levels of indentation collapse:
String city = findByRoll(101)
.map(Student::address)
.map(Address::city)
.map(City::name)
.orElse("unknown");One expression. If anything in the chain is empty, everything after it is skipped and orElse supplies the answer. There is no if, and there is nothing to forget.
Under the hood
Going deeperOptional is a box holding zero or one thing. The whole class is about this:
public final class Optional<T> {
private final T value; // null when empty
...
}It is final and its field is final, which makes it immutable in the Section 8.2 sense. map does not change an Optional. It returns a different one.
There is a null inside. Optional did not remove it, it moved it somewhere you cannot reach by accident.
orElse versus orElseGet is the one to see for yourself. Put a print inside a fallback and call both on an Optional that already has a value:
Optional<String> present = Optional.of("real");
present.orElse(expensive()); // prints ** expensive() ran **, returns "real"
present.orElseGet(Op::expensive); // prints nothing, returns "real"The first one built a fallback it did not use, and it always will. orElse(expensive()) is a normal method call, so Java works out the argument before orElse is entered. Whether the Optional is empty is decided afterwards, far too late.
orElseGet takes a Supplier from Section 11.2. A Supplier is not a value, it is instructions for making one, and instructions can be ignored. This is the same laziness as a stream, in a much smaller place.
Empty chains do nothing, quietly:
Optional.<String>empty().map(String::toUpperCase).orElse("none"); // "none"map on an empty Optional does not call your function. It returns empty, and so does the next one, and the next. The chain runs to the end and every stage skipped its work. That is why the four if statements collapsed into four map calls with nothing in between.
get is the one to avoid. On an empty Optional:
NoSuchElementException: No value presentWhich is the same crash as before, wearing a different name. Java 10 added orElseThrow() with no arguments, doing the same thing with a name that tells the reader you meant it. Newer code uses that, and treats get as a mistake.
Streams and Optionals fit together in both directions. findFirst, min, max and reduce hand you an Optional. Going the other way, Optional.stream() turns one into a stream of zero or one, which lets a list of Optionals flatten:
List<Optional<String>> maybes = ...;
List<String> real = maybes.stream().flatMap(Optional::stream).toList();Every empty one disappears and every present one contributes its value, with no isPresent anywhere.
What it costs
Every Optional is a real object. Wrapping a value allocates one, and a method called in a tight loop that returns an Optional allocates one per call. For most code this does not matter. On a hot path it is why the primitive versions, OptionalInt and the rest, exist at all.
There is also a version of the null problem that Optional cannot fix:
Optional<String> name = null; // legal, and now you have two empty casesThe variable itself can be null. Nothing prevents it, and now a caller has to think about an absent value and an absent Optional. This is one of the reasons Optional is meant for return types and not for fields or parameters, where more code can get at it.
Used badly it makes things worse rather than better. isPresent and get next to each other is a null check with extra objects, and Optional<Optional<T>> is a real thing people write. The type only pays for itself when the value stays inside it.
Optional is also not serialisable, which rules it out for fields in anything that gets written to disk or sent over a network. That is deliberate, and it surprises people who reached for it as a general purpose container.
And it is worth being clear about what it does not do. Optional does not stop NullPointerException from existing. Every library you call can still return null, every field can still be null, and Optional.ofNullable at the boundary is how you deal with it. What changed is that your own methods can now say what they mean.
Check yourself
Answer from memory before opening the explanation. Then compare your rule with the answer and revisit the example if they differ.
`orElse` and `orElseGet` return the same value. What is different?
Show the answer
When the fallback is built.
orElse(expensive())is an ordinary method call, so its argument is worked out beforeorElsestarts. The fallback is built every time, including when the Optional has a value and the fallback is thrown away unused.orElseGet(this::expensive)takes aSupplier. Nothing is built untilorElseGetdecides it needs one, which it only does when the Optional is empty.You can watch it. Put a print inside the fallback, call both on an Optional that has a value, and only
orElseprints.For a constant like
orElse("unknown")it makes no difference. For anything that hits a database, reads a file or allocates, it is the difference between doing the work and not doing it.What is wrong with `if (result.isPresent()) { use(result.get()); }`?
Show the answer
It is a null check with more typing.
You have replaced
if (x != null)withif (x.isPresent())and gained nothing at all. The forgetting is still possible: nothing stops the next person callingget()without theif, and that throws.Optionalis worth having when you never open it. Say what should happen instead:map,filter,orElse,orElseGet,ifPresent,orElseThrow. Each of those handles the empty case as part of asking.If you find yourself writing
isPresentfollowed byget, there is nearly always a single method that does the same job and cannot be got wrong.Why should `Optional` not be used for a field or a method parameter?
Show the answer
Because it was designed for one job: a return type saying "there might be nothing here".
As a field it costs you an extra object per instance for no benefit, and it does not serialise. A field that might be absent is already expressible: the field is null, and inside your own class you control that.
As a parameter it makes the caller worse off. Now they have to wrap their value on the way in, and there are three cases to handle instead of two: a value, an empty Optional, and a null Optional. Two overloads say the same thing more clearly.
Coming back the other way it earns its place, because the caller cannot ignore it without deciding to.
Exercises
Write each solution in VS Code, predict its result, and then run the checks. Reading an example does not replace implementing it.
4 exercises95 pointsabout 95 minutes
Watch the Fallback Run
ex-11-5-aFour calls, and you should be able to predict all four prints before running.
The one that catches people is orElse on an Optional that already has a value. The fallback runs, the answer is thrown away, and the returned value is correct. Nothing looks wrong from the outside.
Then imagine that print is a database query, and read the line again:
findUser(id).orElse(loadDefaultUserFromDatabase());
That is a query on every call, including every call that found the user.
What your program must do
- Predict which of the four calls prints, then run them
- Explain why orElse behaves that way, using how Java evaluates arguments
- Time both in a loop and say what the difference is made of
- Say when you would still reach for orElse
import java.util.*;
public class Fallback {
static String expensive() {
System.out.println(" ** expensive() ran **");
return "fallback";
}
public static void main(String[] args) {
Optional<String> present = Optional.of("real");
Optional<String> empty = Optional.empty();
// TODO: orElse and orElseGet on the PRESENT one. Predict the prints first.
// TODO: the same two on the EMPTY one.
// TODO: put both in a loop of 1000 and time them
// TODO: say when orElse is the better choice anyway
}
}
Hint 1
orElse(expensive()) is an ordinary method call. Java works out every argument before the method starts, so expensive() has already run by the time orElse gets to decide anything.Hint 2
orElseGet takes a Supplier, which is instructions for making a value rather than a value. Instructions can be ignored, and that is the whole difference.Hint 3almost the answer
orElse("unknown") there is nothing to save and it reads better. The rule is about whether building the fallback costs anything, not about which method is newer.Flatten the Four Ifs
ex-11-5-bWrite the ugly version first and count the indentation.
Four levels to print one name, and every level exists because some method might return nothing and did not say so in its signature. That is the code Optional was invented to delete.
Then write the chain. Roll 102 is in there on purpose: the student exists but the address inside is null. Work out what map does with that before you look, because the answer is what makes the chain safe rather than shorter.
What your program must do
- Write it once with isPresent and get, and count the branches
- Write it again with map and orElse, and compare
- Handle roll 102, where a field inside the student is null
- Say what map does when the Optional is already empty
import java.util.*;
public class Flatten {
record City(String name) { }
record Address(City city) { }
record Student(String name, Address address) { }
static Optional<Student> findByRoll(int roll) {
if (roll == 101) return Optional.of(new Student("Aditya", new Address(new City("Pune"))));
if (roll == 102) return Optional.of(new Student("Rohit", null));
return Optional.empty();
}
public static void main(String[] args) {
// TODO: get the city name for roll 101, using isPresent and get. Count the ifs.
// TODO: the same thing with map and orElse. Count the lines.
// TODO: try roll 102, where the address is null. What breaks, and how do you fix it?
// TODO: try roll 999. Predict before running.
}
}
Hint 1
findByRoll(101).map(Student::address).map(Address::city).map(City::name).orElse("unknown"). Each map is one of the old if statements.Hint 2
map handles that for you: a mapping function returning null gives an empty Optional back, not a crash.Hint 3almost the answer
map on an empty Optional never calls your function. It returns empty, and so does every stage after it, which is why the whole chain can be written with nothing in between.Every Way to Make It Wrong
ex-11-5-cAll four of these compile and three of them are in real codebases right now.
Work out the problem before writing the fix. The parameter one is the most interesting: it feels like it is helping the caller, and it is doing the opposite. Count how many cases the caller now has to handle.
Number three is the one that undoes the whole point. A method returning a null Optional gives you back exactly the crash you were trying to make impossible, in a place where nobody thinks to check.
What your program must do
- Say what is wrong with each of the four and write the better version
- Show what happens at the call site when a method returns a null Optional
- Rewrite the isPresent and get version as one expression
- Explain why Optional.of throws on null rather than giving you an empty one
import java.util.*;
public class Misuse {
// TODO: each of these is a bad use. Work out why, then write the better version.
// 1. a field
static class Account { Optional<String> nickname; }
// 2. a parameter
static void greet(Optional<String> name) { }
// 3. a null Optional
static Optional<String> lookup() { return null; }
// 4. isPresent then get
static String describe(Optional<String> o) {
if (o.isPresent()) return o.get().toUpperCase();
return "none";
}
public static void main(String[] args) {
// TODO: show what goes wrong with number 3 at the call site
// TODO: rewrite number 4 as a single expression
// TODO: try Optional.of(null) and read the exception
}
}
Hint 1
Hint 2
o.map(String::toUpperCase).orElse("none"). Same behaviour, one expression, and no way to skip the check.Hint 3almost the answer
Optional.of(x) is you stating that there is a value. If x is null you were wrong about your own data, and finding out immediately beats carrying an empty Optional you believed was full. ofNullable is for when you genuinely do not know.Optionals Into Streams
ex-11-5-dAn Optional is a collection that holds zero things or one thing. Everything here follows from taking that seriously.
Once Optional.stream() exists, a stream of Optionals flattens the same way a stream of lists does with flatMap. The empty ones contribute nothing, the full ones contribute one, and no branch is written anywhere.
Do it both ways at the end and pick one. There is a real argument for each, and being able to make it is more useful than knowing the method names.
What your program must do
- Map the ids through lookup and name the type you end up with
- Flatten to a List of found values with no isPresent anywhere
- Do it a second way and say which reads better
- Count the misses, and find the first hit with a default
import java.util.*;
import java.util.stream.*;
public class Both {
static Optional<String> lookup(int id) {
return id % 3 == 0 ? Optional.empty() : Optional.of("value" + id);
}
public static void main(String[] args) {
List<Integer> ids = List.of(1, 2, 3, 4, 5, 6, 7);
// TODO: map each id through lookup. What type do you have now?
// TODO: get a List<String> of only the ones that were found,
// WITHOUT writing isPresent anywhere
// TODO: do the same again a different way
// TODO: count how many were missing
// TODO: find the first one that was found, and give a default if none were
}
}
Hint 1
Stream<Optional<String>>, which is a stream of boxes. You want a stream of what is in the boxes.Hint 2
.flatMap(Optional::stream) is the direct way. Optional.stream() gives a stream of zero or one, so the empty ones contribute nothing and disappear.Hint 3almost the answer
.filter(Optional::isPresent).map(Optional::get). It works and it says less. Both are used in real code, and the flatMap version is the one that keeps the value inside the box until the last moment.After the credits
Every stream so far has run on one thread, doing one element at a time in a predictable order.
list.parallelStream().filter(...).map(...).toList();One word, and the same pipeline is split across every core in the machine. Section 11.6 is about when that is a win and when it makes things slower, which is more often than the word suggests.
The same section closes the gap left in Section 11.2. Function<Integer, Integer> boxed every value and cost 204 ms against under 1 ms. Stream<Integer> has exactly the same problem, and IntStream, LongStream and DoubleStream are the answer, along with the primitive Optionals mentioned above.
Threads you opened in this section
- OptionalCompletableFuture is the same idea for a value that has not arrived yet.14.11 - Futures That Do Not Block, and Threads That Are Nearly Free
Optional will return in 12.1 - Exceptions, Errors, and the Stack Trace