flatMap: Flattening Nested Streams and Collections

Stream.map() transforms each element into exactly one output element, producing a Stream> when the mapping function itself returns a stream. flatMap() solves this by applying the mapping function and then flattening the inner streams into a single output stream: one level of nesting removed. The same pattern exists on Optional (Java 8) and CompletableFuture, always meaning: apply a function that returns the same wrapper type, then unwrap one layer.

Contents

The Problem flatMap Solves

When a mapping function itself returns a collection or stream, a plain map() produces a Stream<Stream<T>>: a stream of streams. flatMap() applies the mapping function and immediately merges (flattens) the inner streams into a single output stream, removing one level of nesting:

Java
import java.util.*;
import java.util.stream.*;

// Problem: nested List<List<T>> — you want a flat List<T>

List<List<Integer>> matrix = List.of(
    List.of(1, 2, 3),
    List.of(4, 5),
    List.of(6, 7, 8, 9)
);

// Using map() gives you Stream<Stream<Integer>> — still nested
Stream<Stream<Integer>> wrong = matrix.stream()
    .map(row -> row.stream()); // type: Stream<Stream<Integer>>  ← NOT what we want

// flatMap() applies the mapper then flattens one level
List<Integer> flat = matrix.stream()
    .flatMap(row -> row.stream())        // or: .flatMap(Collection::stream)
    .toList();
System.out.println(flat); // [1, 2, 3, 4, 5, 6, 7, 8, 9]

// Same with method reference
List<Integer> flat2 = matrix.stream()
    .flatMap(Collection::stream)
    .toList();

// Example: lines in a file, split into words
List<String> lines = List.of(
    "Hello world",
    "Java streams are powerful",
    "flatMap flattens"
);

List<String> words = lines.stream()
    .flatMap(line -> Arrays.stream(line.split(" ")))
    .toList();
System.out.println(words);
// [Hello, world, Java, streams, are, powerful, flatMap, flattens]

// Unique words, sorted
List<String> uniqueWords = lines.stream()
    .flatMap(line -> Arrays.stream(line.split(" ")))
    .map(String::toLowerCase)
    .distinct()
    .sorted()
    .toList();
System.out.println(uniqueWords);
// [are, flatmap, flattens, hello, java, powerful, streams, world]
flatMap is equivalent to calling map followed by flatten (a concept from functional programming). It removes exactly one level of nesting, if your input is List<List<List<T>>> you would need two flatMap calls to reach a flat stream.

Stream.flatMap() vs map()

map(Function<T,R>) produces one output per input, resulting in Stream<R>. flatMap(Function<T, Stream<R>>) applies the function and concatenates the inner streams: the output is still Stream<R>. A mapper returning an empty stream acts as a filter:

Java
// Signature comparison:
//   map(Function<T, R>)          → Stream<R>
//   flatMap(Function<T, Stream<R>>) → Stream<R>  (streams are merged)

record Order(String id, List<String> items) {}

List<Order> orders = List.of(
    new Order("O1", List.of("apple",  "banana", "cherry")),
    new Order("O2", List.of("date",   "elderberry")),
    new Order("O3", List.of("fig",    "grape", "honeydew"))
);

// map — produces Stream<List<String>>
List<List<String>> nestedItems = orders.stream()
    .map(Order::items)
    .toList();
System.out.println(nestedItems);
// [[apple, banana, cherry], [date, elderberry], [fig, grape, honeydew]]

// flatMap — produces Stream<String> directly
List<String> allItems = orders.stream()
    .flatMap(order -> order.items().stream())
    .toList();
System.out.println(allItems);
// [apple, banana, cherry, date, elderberry, fig, grape, honeydew]

// Combining flatMap with filter and distinct
long uniqueItemCount = orders.stream()
    .flatMap(order -> order.items().stream())
    .distinct()
    .count();
System.out.println("Unique items: " + uniqueItemCount); // 8

// flatMap with a mapper that may return empty streams — acts as filter+map
List<String> longItems = orders.stream()
    .flatMap(order -> order.items().stream()
        .filter(item -> item.length() > 5))  // keep only items with length > 5
    .toList();
System.out.println(longItems);
// [banana, cherry, elderberry, grape, honeydew]

// Another common idiom: produce 0 or 1 element via flatMap+Optional
List<String> upperLong = orders.stream()
    .flatMap(order -> order.items().stream())
    .flatMap(item -> item.length() > 5
        ? Stream.of(item.toUpperCase())
        : Stream.empty())
    .toList();
System.out.println(upperLong);
// [BANANA, CHERRY, ELDERBERRY, GRAPE, HONEYDEW]

flatMap with Optional (Java 9+)

Optional.flatMap() applies a function that itself returns an Optional, unwrapping one layer of wrapping. Without it, chaining nullable navigation produces Optional<Optional<T>>. The Java 9 addition of Optional::stream bridges Stream<Optional<T>> into a flat stream of present values:

Java
import java.util.Optional;

// Optional.flatMap — when the mapper itself returns an Optional
// Avoids Optional<Optional<T>>

record Address(String city, String postcode) {}
record User(String name, Address address) {}

// Nullable-safe chain using flatMap
Optional<User> user = Optional.of(new User("Alice", new Address("London", "EC1A")));

// Without flatMap — Optional<Optional<String>>
Optional<Optional<String>> wrong = user.map(u -> Optional.ofNullable(u.address())
    .map(Address::city));

// With flatMap — Optional<String>
Optional<String> city = user.flatMap(u -> Optional.ofNullable(u.address()))
                             .map(Address::city);
System.out.println(city); // Optional[London]

// Chained nullable navigation
Optional<String> postcode = user
    .flatMap(u  -> Optional.ofNullable(u.address()))
    .flatMap(a  -> Optional.ofNullable(a.postcode()));
System.out.println(postcode); // Optional[EC1A]

// User with null address — chain returns empty without NPE
Optional<User> noAddress = Optional.of(new User("Bob", null));
Optional<String> noCity = noAddress
    .flatMap(u -> Optional.ofNullable(u.address()))
    .map(Address::city);
System.out.println(noCity); // Optional.empty

// Java 9: Optional.stream() — bridge Optional to Stream for flatMap
List<Optional<String>> optionals = List.of(
    Optional.of("hello"),
    Optional.empty(),
    Optional.of("world"),
    Optional.empty(),
    Optional.of("java")
);
// Flatten away empty Optionals — Java 9+
List<String> present = optionals.stream()
    .flatMap(Optional::stream)  // Optional.stream() = 0 or 1 element
    .toList();
System.out.println(present); // [hello, world, java]

// Java 8 alternative (before Optional::stream existed)
List<String> presentJava8 = optionals.stream()
    .filter(Optional::isPresent)
    .map(Optional::get)
    .toList();
Optional::stream was added in Java 9 specifically to bridge Stream<Optional<T>> cleanly. Combined with Stream.flatMap(Optional::stream), it is the idiomatic way to discard empty optionals from a stream without a separate filter step.

flatMap on Arrays

Use Arrays.stream() to convert an array to a stream, then flatMap / flatMapToInt / flatMapToDouble to flatten multi-dimensional arrays. flatMapToInt is the correct overload for int[] rows: it avoids boxing and returns an IntStream:

Java
// Arrays.stream() wraps a T[] or int[] into a Stream
// Combine with flatMap to flatten arrays-of-arrays

String[][] grid = {
    {"a", "b", "c"},
    {"d", "e"},
    {"f", "g", "h", "i"}
};

// flatMap each row (String[]) into its stream
List<String> flat = Arrays.stream(grid)
    .flatMap(Arrays::stream)
    .toList();
System.out.println(flat); // [a, b, c, d, e, f, g, h, i]

// int[][] — use IntStream
int[][] intGrid = {{1, 2, 3}, {4, 5, 6}, {7, 8, 9}};
int[] flatInts = Arrays.stream(intGrid)
    .flatMapToInt(Arrays::stream)  // flatMapToInt for primitive int[]
    .toArray();
System.out.println(Arrays.toString(flatInts)); // [1, 2, 3, 4, 5, 6, 7, 8, 9]

// Sum of all elements across all rows
int total = Arrays.stream(intGrid)
    .flatMapToInt(Arrays::stream)
    .sum();
System.out.println(total); // 45

// Flatten a String[] of CSV lines into tokens
String[] csvLines = {"1,apple,0.50", "2,banana,0.30", "3,cherry,1.20"};
String[] tokens = Arrays.stream(csvLines)
    .flatMap(line -> Arrays.stream(line.split(",")))
    .toArray(String[]::new);
System.out.println(Arrays.toString(tokens));
// [1, apple, 0.50, 2, banana, 0.30, 3, cherry, 1.20]

// flatMapToLong, flatMapToDouble analogues exist for primitive streams
double[] prices = {0.50, 0.30, 1.20};
double[] doubled = Arrays.stream(prices)
    .flatMap(p -> DoubleStream.of(p, p * 2))  // each price becomes two values
    .toArray();
System.out.println(Arrays.toString(doubled)); // [0.5, 1.0, 0.3, 0.6, 1.2, 2.4]

Practical Examples: Sentences to Words, Orders to Line Items

Real-world flatMap use cases include word-frequency counting (sentences → words), e-commerce revenue aggregation (orders → line items), and many-to-many tag indexing (articles → per-tag article lists):

Java
import java.util.*;
import java.util.stream.*;

// --- Example 1: sentences → word frequency map ---
List<String> sentences = List.of(
    "the quick brown fox",
    "the fox jumped over the lazy dog",
    "the dog barked at the fox"
);

Map<String, Long> wordFreq = sentences.stream()
    .flatMap(sentence -> Arrays.stream(sentence.split("\\s+")))
    .collect(Collectors.groupingBy(w -> w, Collectors.counting()));

// Top 3 most frequent words
wordFreq.entrySet().stream()
    .sorted(Map.Entry.<String, Long>comparingByValue().reversed())
    .limit(3)
    .forEach(e -> System.out.println(e.getKey() + ": " + e.getValue()));
// the: 5   fox: 3   dog: 2

// --- Example 2: orders → line items (e-commerce) ---
record LineItem(String productId, int quantity, double unitPrice) {
    double subtotal() { return quantity * unitPrice; }
}
record Order(String orderId, String customerId, List<LineItem> lineItems) {}

List<Order> orders = List.of(
    new Order("O1", "C1", List.of(
        new LineItem("SKU-A", 2, 9.99),
        new LineItem("SKU-B", 1, 24.99))),
    new Order("O2", "C2", List.of(
        new LineItem("SKU-A", 5, 9.99),
        new LineItem("SKU-C", 3, 14.99))),
    new Order("O3", "C1", List.of(
        new LineItem("SKU-B", 2, 24.99)))
);

// Total revenue across all orders
double totalRevenue = orders.stream()
    .flatMap(o -> o.lineItems().stream())
    .mapToDouble(LineItem::subtotal)
    .sum();
System.out.printf("Total revenue: $%.2f%n", totalRevenue); // $199.82

// Units sold per SKU
Map<String, Integer> unitsBySku = orders.stream()
    .flatMap(o -> o.lineItems().stream())
    .collect(Collectors.groupingBy(
        LineItem::productId,
        Collectors.summingInt(LineItem::quantity)));
System.out.println(unitsBySku); // {SKU-A=7, SKU-B=3, SKU-C=3}

// All line items for customer C1
List<LineItem> c1Items = orders.stream()
    .filter(o -> o.customerId().equals("C1"))
    .flatMap(o -> o.lineItems().stream())
    .toList();
System.out.println("C1 items: " + c1Items.size()); // 3

// --- Example 3: tags — many-to-many relationship ---
record Article(String title, List<String> tags) {}
List<Article> articles = List.of(
    new Article("Java Streams",    List.of("java", "streams", "functional")),
    new Article("Spring Boot",     List.of("java", "spring",  "backend")),
    new Article("React Hooks",     List.of("javascript", "react", "frontend")),
    new Article("REST API Design", List.of("backend", "rest",  "api"))
);

// All distinct tags used across articles
List<String> distinctTags = articles.stream()
    .flatMap(a -> a.tags().stream())
    .distinct()
    .sorted()
    .toList();
System.out.println(distinctTags);
// [api, backend, frontend, functional, java, javascript, react, rest, spring, streams]

// Articles per tag
Map<String, List<String>> articlesByTag = articles.stream()
    .flatMap(a -> a.tags().stream().map(tag -> Map.entry(tag, a.title())))
    .collect(Collectors.groupingBy(
        Map.Entry::getKey,
        Collectors.mapping(Map.Entry::getValue, Collectors.toList())));
articlesByTag.forEach((tag, titles) -> System.out.println(tag + ": " + titles));

flatMap vs mapMulti (Java 16+)

Stream.mapMulti() (Java 16+) is a push-based alternative to flatMap(): instead of returning a stream, you push zero or more output elements into a Consumer. It can outperform flatMap for small per-element expansions by avoiding intermediate stream allocation:

Java
// Stream.mapMulti — Java 16+ alternative to flatMap
// Signature: mapMulti(BiConsumer<T, Consumer<R>> mapper) → Stream<R>
// Instead of returning a Stream, you push elements into a Consumer
// Often faster for small expansions (avoids creating intermediate Stream objects)

List<List<Integer>> nested = List.of(
    List.of(1, 2, 3),
    List.of(4, 5),
    List.of(6, 7, 8)
);

// flatMap version
List<Integer> withFlatMap = nested.stream()
    .flatMap(Collection::stream)
    .toList();

// mapMulti version — push each inner element via the consumer
List<Integer> withMapMulti = nested.stream()
    .<Integer>mapMulti((list, consumer) -> list.forEach(consumer))
    .toList();
System.out.println(withFlatMap.equals(withMapMulti)); // true

// mapMulti to expand each element into 0, 1, or many outputs
// Example: duplicate even numbers, skip negatives
List<Integer> numbers = List.of(-3, 1, 2, 3, 4, 5, 6);
List<Integer> result = numbers.stream()
    .<Integer>mapMulti((n, consumer) -> {
        if (n < 0) return;           // skip negatives (0 outputs)
        consumer.accept(n);          // always emit the number
        if (n % 2 == 0) consumer.accept(n); // emit even numbers twice
    })
    .toList();
System.out.println(result); // [1, 2, 2, 3, 4, 4, 5, 6, 6]

// When to prefer flatMap vs mapMulti:
// flatMap    — cleaner when mapper returns an existing Stream or Collection
// mapMulti   — better for performance-critical code with many small outputs,
//              or when the number of outputs depends on complex logic

// Type-safe extraction with mapMulti (like instanceof-filter)
List<Object> mixed = List.of("hello", 42, "world", 3.14, "java", 100);
List<String> strings = mixed.stream()
    .<String>mapMulti((obj, consumer) -> {
        if (obj instanceof String s) consumer.accept(s);
    })
    .toList();
System.out.println(strings); // [hello, world, java]

// Equivalent flatMap version (slightly more verbose)
List<String> stringsFlat = mixed.stream()
    .filter(o -> o instanceof String)
    .map(o -> (String) o)
    .toList();
mapMulti can outperform flatMap when each element expands to only a handful of outputs, because it avoids allocating an intermediate Stream object per element. For large inner collections, flatMap(Collection::stream) remains the clearer and equally efficient choice.