Why should you avoid putting Optional inside collections (e.g. List<Optional<T>> or Optional<List<T>>), and what should you use instead?
answer
- Collections already have an empty state
- Optional<List> = two empty states → return empty list
- List<Optional<T>> = per-element alloc + unwrap-everywhere
- flatMap(Optional::stream) / filter(Objects::nonNull) to drop absences
- Effective Java: return empty collections, not null/Optional
basics
~20 sCollections already have a natural 'empty' — an empty list or set — and elements that aren't there simply aren't added. Wrapping elements in Optional, or wrapping a whole collection in Optional, just adds clutter and ambiguity. Return an empty collection instead of Optional<List>, and filter out missing elements instead of storing Optional<T> in the list.
solid answer
~40 sPutting Optional inside or around a collection is redundant and confusing. A collection already expresses 'nothing' as an empty collection, so a method returning a list should return an empty list, never Optional<List<T>> — that creates two empty states (null/absent Optional vs empty list) and forces callers into pointless unwrapping. Likewise List<Optional<T>> is an anti-pattern: it scatters allocations, complicates every iteration, and begs the question of what an empty slot even means. The idioms: return Collections.emptyList()/List.of() for 'no results'; build collections by filtering absent values out (e.g. stream().filter(Objects::nonNull) or flatMap(Optional::stream)) so the result holds only present values. The guiding principle from Effective Java is to return empty collections, not null and not Optional, for multi-valued results — Optional is for a single possibly-absent value.
code
java · 18 lines// Anti-patterns
Optional<List<Order>> findOrders(Id id); // wrap whole collection
List<Optional<Order>> rows; // wrap each element
Map<String, Optional<User>> cache; // Optional as map value
// Idiomatic
List<Order> findOrders(Id id) { // empty list when none
return repo.lookup(id); // never null, never Optional
}
// Build a list of only present values
List<Order> present = rows.stream()
.flatMap(Optional::stream) // Java 9+: drops empties
.toList();
List<User> nonNull = raw.stream()
.filter(Objects::nonNull)
.toList();go deeper
Knows to return an empty list rather than Optional<List> and not to fill a list with Optionals.
Explains the 'two empty states' problem and uses filter/flatMap(Optional::stream) to build present-only collections.
Cites the Effective Java rule, distinguishes single-absent (Optional) from zero-or-more (collection), and treats Stream<Optional> as a valid transient only.
Bakes 'collections return empty, never Optional/null' into API guidelines and reviews data-model shapes to keep absence encoded in exactly one place.
## Two distinct shapes, both anti-patterns ### 1. Optional<Collection> (wrapping the whole collection) A method that returns multiple values returns a `List`, `Set`, or `Map`. 'No results' is naturally expressed by an **empty** collection. Returning `Optional<List<T>>` is wrong because: - It manufactures **two** ways to say 'nothing': the Optional can be empty, *and* the contained list can be empty. Callers must handle both, and the semantics ('what is the difference between absent and empty?') are unclear. - It forces every caller to unwrap before iterating, adding ceremony for no benefit. - *Effective Java* (Item: 'Return empty collections or arrays, not nulls') explicitly says multi-valued accessors should return empty collections. Optional is the single-value analogue; collections already have their own empty. ```java // BAD Optional<List<Order>> findOrders(Id id); // GOOD List<Order> findOrders(Id id); // empty list when none ``` ### 2. Collection<Optional<T>> (wrapping the elements) `List<Optional<T>>` puts an Optional in every slot. Problems: - **Allocation explosion:** one Optional object per element, all to encode 'this element might be missing' — but a missing element should simply not be in the list. - **Iteration pain:** every loop, stream, or lookup must unwrap, doubling the code and re-introducing the present/absent branch at each step. - **Ambiguous meaning:** what does an empty Optional at index 3 represent versus the element being absent? It is rarely well-defined. The correct move is to **filter absence out as you build the collection** so the result contains only present values: ```java // From a stream of Optionals (Java 9+ Optional::stream is the clean tool): List<T> present = optionals.stream() .flatMap(Optional::stream) // drops empties .toList(); // Or pre-9: List<T> present = optionals.stream() .filter(Optional::isPresent) .map(Optional::get) .toList(); // Filtering raw nullable values: List<T> nonNull = raw.stream().filter(Objects::nonNull).toList(); ``` ## Map values Also avoid `Map<K, Optional<V>>`. A map already encodes absence via `containsKey`/`get`-returns-null, and Java provides `getOrDefault`, `computeIfAbsent`, and (Java 9+) `Optional.ofNullable(map.get(k))` at the *read* site if you want Optional ergonomics — without storing Optionals in the map. ## Why the rule holds Optional's purpose is to make the *single* possibly-absent value explicit at an API boundary. Collections already solve multiplicity *and* absence (via emptiness). Combining them duplicates the 'nothing' concept, multiplies allocations, and complicates traversal — pure cost, no benefit. The discipline: **single absent value → Optional; zero-or-more values → (possibly empty) collection; never both.** ## Nuance A transient `Stream<Optional<T>>` in the middle of a pipeline is fine and common (it is the intermediate before `flatMap(Optional::stream)`). The anti-pattern is *storing* or *returning* collections of Optionals or Optional-of-collection as an API/field shape.
- What does Optional::stream do and why is it useful here?Optional.stream() (Java 9+) returns a Stream of zero or one element. Used with flatMap, it lets a Stream<Optional<T>> collapse to a Stream<T> that contains only the present values, cleanly discarding the empties without an isPresent/get pair.
- Is a Stream<Optional<T>> ever acceptable?Yes, as a transient intermediate inside a pipeline — it is exactly what you flatMap with Optional::stream to drop absences. The anti-pattern is persisting it: returning or storing List/Map of Optionals as an API shape or field.
saying these in an interview costs you the question
- Returning Optional<List<T>> instead of an empty list
- Storing List<Optional<T>> or Map<K, Optional<V>> as the data model
- Returning null for an empty collection (the opposite over-correction)
- Claiming there is a meaningful difference between 'absent Optional list' and 'empty list' for a normal accessor