🌊 Section 8 · Question #29

How to work with Optional in Stream

Optional and the Stream API are designed to work together seamlessly across three foundational scenarios:


🟢 Junior Level

30-Second Summary

Optional<T> and the Stream<T> API are designed to work together seamlessly across three foundational scenarios:

  1. Terminal Stream Operations Returning Optional: Methods like findFirst(), findAny(), min(), max(), and the single-argument reduce() return Optional<T> to safely represent the possibility that the source stream is empty.
  2. Unwrapping Stream<Optional<T>> into Stream<T>: Modern Java (Java 9+) standardizes on .flatMap(Optional::stream), which automatically unpacks present values and silently discards empty containers.
  3. Safe Value Extraction: Calling .get() directly is an anti-pattern. Instead, safely resolve Optional values using .orElse(defaultValue), .orElseGet(Supplier), or .orElseThrow().

Java 21 Code Examples

import java.util.List;
import java.util.Optional;

public class OptionalStreamDemo {

    public record User(int id, String name, String email) {}

    public static void main(String[] args) {
        List<Optional<String>> rawEmails = List.of(
            Optional.of("user1@example.com"),
            Optional.empty(),
            Optional.of("user3@example.com")
        );

        // 1. Transforming Stream<Optional<String>> into Stream<String> (Java 9+)
        List<String> validEmails = rawEmails.stream()
            .flatMap(Optional::stream) // Discards empty Optionals automatically
            .map(String::toLowerCase)
            .toList();

        System.out.println("Valid emails: " + validEmails); // [user1@example.com, user3@example.com]

        // 2. Safe extraction from terminal operations
        List<User> users = List.of(new User(1, "Ivan", "ivan@mail.com"));

        String foundName = users.stream()
            .filter(u -> u.id() == 2)
            .findFirst()
            .map(User::name)
            .orElse("Guest");

        System.out.println("User name: " + foundName); // Guest
    }
}

🟡 Middle Level

Evolution of Unwrapping: Java 8 vs. Java 9+

Java 8 Approach (Two-Step Filtering & Extraction):

List<String> listJava8 = rawOptionals.stream()
    .filter(Optional::isPresent)
    .map(Optional::get) // Verbose and triggers static analysis warnings
    .toList();

Java 9+ Modern Idiom (Optional.stream()):

The Optional.stream() method converts an Optional into a stream:

  • If a value is present $\rightarrow$ returns Stream.of(value) (a single-element stream).
  • If empty $\rightarrow$ returns Stream.empty() (an empty stream).

When chained with .flatMap(), all single-element streams are concatenated into the main pipeline while empty sub-streams are discarded:

List<String> listJava9 = rawOptionals.stream()
    .flatMap(Optional::stream) // Idiomatic, safe, and concise
    .toList();

orElse() vs. orElseGet() Within Stream Pipelines

A common performance pitfall is improper fallback computation:

// ❌ CRITICAL PERFORMANCE PITFALL: orElse() ALWAYS executes the fallback method!
User user = optUser.orElse(userRepository.fetchDefaultUserFromDb());

// ✅ OPTIMAL: orElseGet() executes the supplier ONLY when the Optional is empty!
User user = optUser.orElseGet(userRepository::fetchDefaultUserFromDb);

In stream processing, passing heavy operations (database queries, cryptographic hashing, remote RPCs) to .orElse() forces the JVM to execute that heavy method eagerly for every single item processed, regardless of whether the Optional contains a value.


🔴 Senior Level

Enterprise Architectural Guidelines for Optional

According to OpenJDK architectural guidelines (led by Brian Goetz):

  1. Optional is Strictly for Method Return Types: Optional was created to serve as a library-level method return type where the absence of a value is a normal, expected outcome (e.g. findFirst(), findById()).
  2. Anti-Pattern: Optional in JPA Entity and DTO Fields:
    • java.util.Optional does not implement java.io.Serializable.
    • Placing Optional fields in JPA entities causes serialization failures in distributed caches (Redis, Hazelcast) and session replicators.
    • Increases heap memory footprint by adding 16–24 bytes of pointer indirection overhead per entity field.
  3. Anti-Pattern: Optional as Method Parameters: Passing Optional<T> as a parameter forces callers to wrap arguments (service.update(Optional.of(name))) and introduces the risk of callers passing null directly, defeating the purpose of the container.
  4. The Optional.of(null) Trap: Optional.of(null) immediately throws a NullPointerException. Always use Optional.ofNullable(value) when wrapping nullable references.

High-Throughput GC Optimization: Primitive Specializations

In highload environments (financial exchanges, RTB ad auctions):

  • Allocating millions of heap-based Optional<Integer> instances creates significant garbage collection pressure.
  • Always prefer primitive specialized streams and optionals:
    • IntStream.max() $\rightarrow$ OptionalInt
    • LongStream.findFirst() $\rightarrow$ OptionalLong
    • DoubleStream.average() $\rightarrow$ OptionalDouble

These store values directly in scalar primitive fields with a primitive boolean flag, eliminating object boxing and heap allocation.


Modern Functional Chaining (Java 9–11)

// 1. or() (Java 9+): Fallback pipeline chaining
Optional<User> user = findInCache(id)
    .or(() -> findInDatabase(id))
    .or(() -> findInExternalApi(id));

// 2. ifPresentOrElse() (Java 9+): Branching side effects without if-else
findUser(id).ifPresentOrElse(
    this::sendWelcomeEmail,
    () -> log.warn("User not found: {}", id)
);

// 3. isEmpty() (Java 11+): Readable inverse of isPresent()
if (userOpt.isEmpty()) {
    // Handle absence
}

4 Tricky Questions

1. How does .flatMap(Optional::stream) work internally, and why is it preferred over Java 8’s filter(Optional::isPresent).map(Optional::get)?

Answer: Optional.stream() returns a 1-element Stream.of(value) if the container is non-empty, and an empty Stream.empty() if it is empty.

When combined with flatMap(), the outer pipeline flattens each sub-stream: present values are forwarded downstream, and empty streams produce zero elements. This replaces Java 8’s two-stage filter().map() pipeline with a single declarative stage, eliminates static analyzer warnings on calling .get(), and avoids redundant state checks.


2. What is the performance difference between .orElse(heavyMethod()) and .orElseGet(this::heavyMethod) when applied to stream elements?

Answer:

  • .orElse(heavyMethod()) evaluates eagerly. Java evaluates method arguments prior to invoking orElse(). Consequently, heavyMethod() is executed on every iteration, even when the Optional contains a present value.
  • .orElseGet(this::heavyMethod) accepts a Supplier functional interface and evaluates lazily. The lambda is invoked only if Optional.isEmpty() is true, preventing unnecessary execution and wasted CPU/IO resources.

3. Why should Optional never be used as a field in a JPA Entity or a DTO?

Answer:

  1. Non-Serializable: java.util.Optional does not implement java.io.Serializable. Attempting to serialize an entity containing an Optional field (e.g. for Redis caching or session storage) throws NotSerializableException.
  2. JPA Incompatibility: The JPA and Hibernate specifications do not natively support mapping Optional fields to database table columns.
  3. Memory Bloat: Each Optional is an extra object on the heap (occupying 16 to 24 bytes plus references), inflating heap memory consumption across large datasets.

4. What is the difference between Optional.of() and Optional.ofNullable() when encountering a null reference?

Answer:

  • Optional.of(value) enforces a non-null precondition (Objects.requireNonNull(value)). Passing null immediately throws a NullPointerException. It should only be used when null represents a program bug.
  • Optional.ofNullable(value) checks for null: if the argument is null, it safely returns Optional.empty(). It is the idiomatic factory for wrapping references that may legitimately be null.

🎯 Interview Cheat Sheet

Optional Stream Interaction Matrix

| Task | Recommended Idiom | Anti-Pattern to Avoid | | :— | :— | :— | | Flatten Stream<Optional<T>> | .flatMap(Optional::stream) | .filter(Optional::isPresent).map(Optional::get) | | Fallback Value | .orElseGet(this::calculate) | .orElse(calculate()) (Eager waste) | | Throw on Missing | .orElseThrow(() -> new NotFoundException()) | .get() without check (NoSuchElementException) | | Wrap Nullable | Optional.ofNullable(val) | Optional.of(val) (Throws NPE on null) | | Primitive Streams | OptionalInt, OptionalLong | Boxing into Optional<Integer> |


Key Takeaways

  • Java 9+ Standard: Flatten optional streams using stream.flatMap(Optional::stream).
  • Lazy vs Eager: Always prefer orElseGet(Supplier) over orElse(T) when the fallback value is computationally expensive.
  • Scope of Use: Restrict Optional to method return types; never use it in fields or method parameters.
  • Primitive Efficiency: Use OptionalInt, OptionalLong, and OptionalDouble to minimize heap memory overhead.

Red Flags & Anti-Patterns to Avoid

  • ❌ Calling .get() without .isPresent(): Throws unhandled NoSuchElementException.
  • ❌ Using Optional as entity/DTO fields: Breaks serialization and wastes heap memory.
  • ❌ Passing Optional as a method argument: Complicates API design and invites null arguments.
  • ❌ Calling expensive methods inside orElse(): Executes eagerly on every stream element.