Contents
- The Problem flatMap Solves
- Stream.flatMap() vs map()
- flatMap with Optional (Java 9+)
- flatMap on Arrays
- Practical Examples
- flatMap vs mapMulti (Java 16+)
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:
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.
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:
// 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]
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:
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.
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:
// 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]
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):
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));
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:
// 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.