How to work with Optional in Stream
Optional
🟢 Junior Level
30-Second Summary
Optional<T> and the Stream<T> API are designed to work together seamlessly across three foundational scenarios:
- Terminal Stream Operations Returning
Optional: Methods likefindFirst(),findAny(),min(),max(), and the single-argumentreduce()returnOptional<T>to safely represent the possibility that the source stream is empty. - Unwrapping
Stream<Optional<T>>intoStream<T>: Modern Java (Java 9+) standardizes on.flatMap(Optional::stream), which automatically unpacks present values and silently discards empty containers. - Safe Value Extraction: Calling
.get()directly is an anti-pattern. Instead, safely resolveOptionalvalues 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):
Optionalis Strictly for Method Return Types:Optionalwas 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()).- Anti-Pattern:
Optionalin JPA Entity and DTO Fields:java.util.Optionaldoes not implementjava.io.Serializable.- Placing
Optionalfields 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.
- Anti-Pattern:
Optionalas Method Parameters: PassingOptional<T>as a parameter forces callers to wrap arguments (service.update(Optional.of(name))) and introduces the risk of callers passingnulldirectly, defeating the purpose of the container. - The
Optional.of(null)Trap:Optional.of(null)immediately throws aNullPointerException. Always useOptional.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$OptionalIntLongStream.findFirst()$\rightarrow$OptionalLongDoubleStream.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 invokingorElse(). Consequently,heavyMethod()is executed on every iteration, even when theOptionalcontains a present value..orElseGet(this::heavyMethod)accepts aSupplierfunctional interface and evaluates lazily. The lambda is invoked only ifOptional.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:
- Non-Serializable:
java.util.Optionaldoes not implementjava.io.Serializable. Attempting to serialize an entity containing anOptionalfield (e.g. for Redis caching or session storage) throwsNotSerializableException. - JPA Incompatibility: The JPA and Hibernate specifications do not natively support mapping
Optionalfields to database table columns. - Memory Bloat: Each
Optionalis 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)). Passingnullimmediately throws aNullPointerException. It should only be used whennullrepresents a program bug.Optional.ofNullable(value)checks fornull: if the argument isnull, it safely returnsOptional.empty(). It is the idiomatic factory for wrapping references that may legitimately benull.
🎯 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)overorElse(T)when the fallback value is computationally expensive. - Scope of Use: Restrict
Optionalto method return types; never use it in fields or method parameters. - Primitive Efficiency: Use
OptionalInt,OptionalLong, andOptionalDoubleto minimize heap memory overhead.
Red Flags & Anti-Patterns to Avoid
- ❌ Calling
.get()without.isPresent(): Throws unhandledNoSuchElementException. - ❌ Using
Optionalas entity/DTO fields: Breaks serialization and wastes heap memory. - ❌ Passing
Optionalas a method argument: Complicates API design and invitesnullarguments. - ❌ Calling expensive methods inside
orElse(): Executes eagerly on every stream element.