skip to content

Why should you avoid putting Optional inside collections (e.g. List<Optional<T>> or Optional<List<T>>), and what should you use instead?

level: seniorimportance: should knowfreq 35%

answer

  1. Collections already have an empty state
  2. Optional<List> = two empty states → return empty list
  3. List<Optional<T>> = per-element alloc + unwrap-everywhere
  4. flatMap(Optional::stream) / filter(Objects::nonNull) to drop absences
  5. Effective Java: return empty collections, not null/Optional

basics

~20 s

Collections 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 s

Putting 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
java
// 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

for a junior

Knows to return an empty list rather than Optional<List> and not to fill a list with Optionals.

for a middle

Explains the 'two empty states' problem and uses filter/flatMap(Optional::stream) to build present-only collections.

for a senior

Cites the Effective Java rule, distinguishes single-absent (Optional) from zero-or-more (collection), and treats Stream<Optional> as a valid transient only.

for a principal

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

context