Contents

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.