Optional in Java

Optional makes absence part of the type, so a caller cannot forget that a value might not be there.

The problem

public Note findById(long id) {
    return null;        // nothing in the signature warns the caller
}

Note note = repository.findById(42);
System.out.println(note.title());     // NullPointerException, eventually

A method returning null gives no hint that it might. Optional<T> puts the possibility into the type, so the compiler forces the caller to think about it.

public Optional<Note> findById(long id) {
    return Optional.empty();
}

String title = repository.findById(42)
        .map(Note::title)
        .orElse("not found");

Creating an Optional

Optional<String> present = Optional.of("java");        // throws if the value is null
Optional<String> maybe = Optional.ofNullable(lookup()); // null becomes empty
Optional<String> empty = Optional.empty();

Use of when null would be a bug, and ofNullable when it is a legitimate outcome.

Getting a value out

Optional<Note> found = repository.findById(42);

String a = found.map(Note::title).orElse("unknown");            // a default
String b = found.map(Note::title).orElseGet(this::defaultTitle); // computed lazily
Note   c = found.orElseThrow();                                  // NoSuchElementException
Note   d = found.orElseThrow(() -> new NoteNotFoundException(42));

found.ifPresent(note -> System.out.println(note.title()));
found.ifPresentOrElse(
        note -> System.out.println(note.title()),
        () -> System.out.println("nothing found"));
orElse evaluates its argument every time, even when a value is present. orElseGet takes a supplier and calls it only when empty. When the default is expensive, or has a side effect, that difference matters.
// The database is queried even when the optional has a value
found.orElse(loadFromDatabase());

// Queried only when empty
found.orElseGet(() -> loadFromDatabase());

Transforming without unwrapping

Optional<String> upper = repository.findById(42)
        .filter(Note::isPublished)
        .map(Note::title)
        .map(String::toUpperCase);

Optional<String> author = repository.findById(42)
        .flatMap(Note::authorName);        // when the method itself returns Optional
MethodPurpose
mapTransform the value if present
flatMapTransform when the function returns an Optional
filterKeep the value only if it matches
orSupply an alternative Optional
streamZero or one element, for use in a pipeline
isPresent, isEmptyTest, but prefer the methods above
Optional<Note> note = repository.findById(id)
        .or(() -> archive.findById(id));      // fall back to a second source

Replacing null checks

// Nested null checks
String city = null;
if (user != null) {
    Address address = user.getAddress();
    if (address != null) {
        city = address.getCity();
    }
}
if (city == null) {
    city = "unknown";
}

// The same intent, expressed once
String city = Optional.ofNullable(user)
        .map(User::getAddress)
        .map(Address::getCity)
        .orElse("unknown");

With streams

Optional<Note> mostViewed = notes.stream()
        .max(Comparator.comparingInt(Note::views));

List<Note> found = ids.stream()
        .map(repository::findById)     // Stream<Optional<Note>>
        .flatMap(Optional::stream)     // drops the empties
        .toList();

Several terminal operations return an Optional, precisely because an empty stream has no result to give.

Where Optional does not belong

Use it forDo not use it for
A return type that may have no resultFields of a class
Chaining transformations over a maybe valueMethod parameters
Making absence explicit in an APICollections; return an empty one instead
Anything serialised, as it is not Serializable
// Poor: the caller must wrap every argument
public void save(Optional<String> title) { }

// Better: overload, or accept null with documentation
public void save(String title) { }
public void save() { }

// Poor: an empty list already means "nothing"
public Optional<List<Note>> findAll() { }

// Better
public List<Note> findAll() { return List.of(); }

Common mistakes

  • Calling get() without checking. It was renamed in spirit to orElseThrow() for good reason.
  • Writing if (opt.isPresent()) { opt.get() }, which is the null check with more typing.
  • Using orElse with an expensive or side effecting expression.
  • Returning Optional<Collection> instead of an empty collection.
  • Using Optional as a field or a parameter type.
  • Returning null from a method declared to return Optional, which is the worst of both worlds.

Best practices

  • Use it as a return type for a lookup that may find nothing.
  • Prefer map, filter and orElseGet over explicit presence tests.
  • Use orElseThrow with a specific exception when absence is a real error.
  • Never return null from a method that returns Optional.
  • Keep it out of fields, parameters and serialised types.

Practice

  1. Rewrite a three level nested null check as a single Optional chain.
  2. Explain the difference between orElse(expensive()) and orElseGet(this::expensive) with a print statement.
  3. When would flatMap be needed rather than map?
  4. Why should a repository return List.of() rather than Optional<List>?
  5. Convert a method that returns null into one returning Optional, and update two call sites.

Conclusion

Optional exists to make absence visible in a method signature. Use it for return values, chain with map and filter rather than unwrapping early, and keep it out of fields and parameters.

Written by Lorens Mishra

Default administrator account created by the installer.

Continue reading

All Java notes →
Java

Introduction to Java

Java is a statically typed, object oriented language that compiles to bytecode and runs on a virtual machine, which is what makes it portable.

Read more

Discussion

0 comments
Sign in to join the discussion.

No comments yet. Be the first to say something.