Table of Contents

flatmap in Java 8 streams

  • Last Updated: March 10, 2024
  • By: javahandson
  • Series
img

flatmap in Java 8 streams

The flatMap in Java 8 streams solves one very specific problem. You have a stream where every element hides a smaller collection inside it, and you want one flat stream of the inner values instead. In this article we will build that idea from scratch, look at all four flatMap variants, and finish with the mistakes that trip up almost every beginner.

1. Introduction

Real data loves to nest. An order holds a list of items. A student holds a list of subjects. A sentence holds a list of words. Sooner or later you want to work with all those inner values at once, as a single flat sequence.

That is exactly the job of flatMap. It takes each element, turns it into a small stream, and then pours all those small streams into one big stream. No nesting left over.

Beginners often reach for map first and end up with a Stream<List<String>> that refuses to cooperate. Once the difference clicks, a whole class of problems becomes a two-line pipeline.

1.1 What This Article Covers

  • What flattening means, with a picture you can hold in your head
  • The flatMap signature, and how to read those scary generics
  • Five everyday shapes of nested data and the pipeline for each
  • How map and flatMap differ, side by side
  • The primitive cousins: flatMapToInt, flatMapToLong and flatMapToDouble
  • Laziness, stream closing, short-circuiting and parallel behaviour
  • Seven mistakes that cost beginners an afternoon each
  • A small end-to-end program, plus interview questions

2. What Flattening Actually Means

Flattening removes one layer of nesting. That is the whole idea. Everything else in this article follows from that one sentence.

2.1 The Nested Box Analogy

Picture three cardboard boxes on a table. Each box holds a handful of marbles. You want to count the marbles.

Counting boxes gives you three. That number tells you nothing useful. So you tip every box out onto the table and now you see all the marbles together, loose, in one pile.

Tipping the boxes out is flattening. The boxes vanish, the marbles remain. In stream terms, Stream<List<Marble>> becomes Stream<Marble>.

2.2 The Problem map Cannot Solve

Say you have two words and you want every distinct character across both of them. Splitting a word gives you an array. So a first attempt with map looks reasonable.

List<String> words = Arrays.asList("Learning", "Java");

List<String[]> result = words.stream()
        .map(word -> word.split(""))
        .distinct()
        .collect(Collectors.toList());

System.out.println(result);
// Output: [[Ljava.lang.String;@8db2f2, [Ljava.lang.String;@18bf509]

That output looks like garbage because it is two array objects printed by their default toString. The pipeline never saw a single character. It saw two arrays.

The distinct call compared arrays against arrays, not letters against letters. Two different arrays are never equal, so nothing got removed. We are one layer too high.

2.3 Where flatMap Sits in a Pipeline

Every stream pipeline has three parts: a source, some intermediate operations, and one terminal operation. Like filter, map and sorted, the flatMap method belongs to the middle group.

  • It returns another Stream, so you can keep chaining
  • Nothing runs until a terminal operation asks for values
  • Your original lists and arrays stay exactly as they were
  • Most pipelines place it early, so every later step sees flat data

That last point matters more than it sounds. Flatten first and your filter, sorted and collect calls all work on plain elements instead of fighting a layer of wrappers.

New to pipelines altogether? Start with introduction to streams in Java and then come back here.

3. The flatMap Method

The Stream interface declares flatMap as an intermediate operation. You hand it a function. That function must turn one element into a stream of new values. Java then splices all of those streams together.

3.1 Syntax of flatMap

<R> Stream<R> flatMap(Function<? super T, ? extends Stream<? extends R>> mapper)

Type parameter:
R      - the element type of the new stream

Parameter:
mapper - a stateless function applied to each element,
         which must return a Stream of new values

Returns: a new stream holding the contents of every mapped stream

The wildcards look heavy, so read only the shape. You give it T, it wants back a Stream of R, and the whole call hands you a Stream<R>.

That middle bit deserves a highlight. Your function returns a stream, never a list and never an array. Forgetting that causes the most common compile error with this method.

3.2 Unique Characters From a List of Words

Now let us fix the broken example from section 2.2. We keep the split, then flatten the arrays into a stream of single characters.

package com.javahandson.flatmap;

import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;

public class UniqueCharacters {
    public static void main(String[] args) {

        List<String> words = Arrays.asList("Learning", "Java");

        List<String> unique = words.stream()
                .map(word -> word.split(""))
                .flatMap(Arrays::stream)
                .distinct()
                .collect(Collectors.toList());

        System.out.println(unique);
        // Output: [L, e, a, r, n, i, g, J, v]
    }
}

One extra line changed everything. The distinct call now compares strings of length one, so the repeated letters drop out.

3.3 Reading the Pipeline Step by Step

  • Source: the list gives us Stream<String> holding “Learning” and “Java”
  • map: each word splits into a String[], so we hold Stream<String[]>
  • flatMap: Arrays::stream turns each array into a small stream, and those merge into one Stream<String>
  • distinct: duplicate letters such as the second “n” and the second “a” disappear
  • collect: the terminal operation gathers everything into a list

Notice how the element type walks down a level at the flatMap step. Before it, we had a stream of arrays. After it, a stream of plain strings.

3.4 The Same Thing in One Lambda

You do not need a separate map stage at all. Do the split inside the flatMap lambda and the pipeline gets shorter.

List<String> unique = words.stream()
        .flatMap(word -> Arrays.stream(word.split("")))
        .distinct()
        .collect(Collectors.toList());

System.out.println(unique);
// Output: [L, e, a, r, n, i, g, J, v]

Both versions produce identical results. Pick whichever reads better to you. The two-stage form helps while you learn, since each type change stays visible.

The Arrays::stream shorthand is a method reference. If that syntax feels new, the method reference in Java 8 article walks through all four kinds.

4. flatMap With Everyday Data Shapes

Nested data shows up in many disguises. Here are the five you will meet most often, each with the pipeline that flattens it.

4.1 A List Inside a List

List<List<String>> nested = Arrays.asList(
        Arrays.asList("Java", "Python"),
        Arrays.asList("Go", "Rust"),
        Arrays.asList("C"));

List<String> flat = nested.stream()
        .flatMap(List::stream)
        .collect(Collectors.toList());

System.out.println(flat);
// Output: [Java, Python, Go, Rust, C]

This one is the textbook case. List::stream gives the inner stream and flatMap does the pouring. Inner lists of different sizes cause no trouble at all.

4.2 A 2D Array Into a 1D Stream

String[][] grid = {
        {"Hello", "we"},
        {"are", "learning"},
        {"flatMap", "method"}
};

List<String> words = Arrays.stream(grid)
        .flatMap(Arrays::stream)
        .collect(Collectors.toList());

System.out.println(words);
// Output: [Hello, we, are, learning, flatMap, method]

A 2D array of objects is really an array of arrays. Arrays.stream(grid) hands you Stream<String[]>, and the second Arrays::stream opens each row.

4.3 Objects That Hold Collections

This shape dominates real code. A domain object owns a list, and you want every value from every object.

class Student {
    private final String name;
    private final List<String> subjects;

    Student(String name, List<String> subjects) {
        this.name = name;
        this.subjects = subjects;
    }

    public List<String> getSubjects() {
        return subjects;
    }
}

List<Student> students = Arrays.asList(
        new Student("Riya", Arrays.asList("Maths", "Physics")),
        new Student("Sam", Arrays.asList("Physics", "Chemistry")),
        new Student("Neha", Arrays.asList("Maths", "Biology")));

List<String> allSubjects = students.stream()
        .flatMap(student -> student.getSubjects().stream())
        .distinct()
        .sorted()
        .collect(Collectors.toList());

System.out.println(allSubjects);
// Output: [Biology, Chemistry, Maths, Physics]

Read the lambda out loud: for each student, give me a stream of their subjects. Java handles the merging. Sorting the flat stream afterwards costs one more call.

4.4 Values Hiding Inside a Map

Map<String, List<String>> teams = new LinkedHashMap<>();
teams.put("Backend", Arrays.asList("Riya", "Sam"));
teams.put("Frontend", Arrays.asList("Neha"));
teams.put("QA", Arrays.asList("Arun", "Meera"));

List<String> everyone = teams.values().stream()
        .flatMap(List::stream)
        .collect(Collectors.toList());

System.out.println(everyone);
// Output: [Riya, Sam, Neha, Arun, Meera]

A Map has no stream() method of its own. Start from values(), keySet() or entrySet(), then flatten whatever collection sits inside.

4.5 Pairing Two Lists Together

Here is a use that surprises people. Nothing is nested yet, but flatMap can create the nesting and flatten it in one move.

List<String> sizes = Arrays.asList("S", "M");
List<String> colours = Arrays.asList("Red", "Blue");

List<String> variants = sizes.stream()
        .flatMap(size -> colours.stream()
                                .map(colour -> size + "-" + colour))
        .collect(Collectors.toList());

System.out.println(variants);
// Output: [S-Red, S-Blue, M-Red, M-Blue]

The outer lambda produces a small stream of pairs for one size. flatMap joins those small streams. Two nested loops just became four lines.

5. map Versus flatMap

This comparison is the single most asked interview question about either method, so let us make it crisp.

5.1 The One-In, Many-Out Rule

With map, one element in means exactly one element out. Ten elements go in, ten come out, every time.

With flatMap, one element in means zero, one, or many elements out. Ten elements can become forty, or seven, or none.

List<List<Integer>> data = Arrays.asList(
        Arrays.asList(1, 2, 3),
        Arrays.asList(4, 5));

// map keeps the nesting: 2 elements in, 2 elements out
System.out.println(data.stream().map(List::size).collect(Collectors.toList()));
// Output: [3, 2]

// flatMap removes it: 2 elements in, 5 elements out
System.out.println(data.stream().flatMap(List::stream).collect(Collectors.toList()));
// Output: [1, 2, 3, 4, 5]

5.2 Side by Side

PointmapflatMap
Mapper returnsA single valueA Stream of values
Elements outAlways the same countZero, one or many per element
NestingKeeps itRemoves one layer
Typical inputStream<Employee>Stream<List<Employee>>
Typical outputStream<String>Stream<Employee>
Common usePull one field outMerge inner collections
Can drop elementsNoYes, via an empty stream

5.3 Which One Should You Pick?

Ask one question: after my lambda runs, do I hold a value or a bag of values? A value calls for map. A bag calls for flatMap.

Another quick test works on the type. If your pipeline shows Stream<List<X>>, Stream<X[]> or Stream<Stream<X>> and you did not want that, flatMap is the fix.

For everything map itself can do, see mapping in Java 8 streams.

6. flatMap Can Also Drop Elements

Most tutorials stop at flattening. But because your mapper returns a stream, that stream can be empty, and an empty stream contributes nothing.

6.1 Returning an Empty Stream

List<String> input = Arrays.asList("12", "abc", "7", "x9");

List<Integer> numbers = input.stream()
        .flatMap(text -> text.matches("\\d+")
                ? Stream.of(Integer.parseInt(text))
                : Stream.empty())
        .collect(Collectors.toList());

System.out.println(numbers);
// Output: [12, 7]

Good values yield a one-element stream. Bad values yield Stream.empty() and quietly vanish. One pass does the checking and the converting together.

6.2 Filter and Flatten in One Step

You can also filter inside the inner stream. Say we want only the long subject names across all students.

List<String> longNames = students.stream()
        .flatMap(student -> student.getSubjects().stream()
                                   .filter(subject -> subject.length() > 5))
        .distinct()
        .collect(Collectors.toList());

System.out.println(longNames);
// Output: [Physics, Chemistry, Biology]

Should you always write it this way? No. A plain filter after the flatMap often reads better. Reach for the inner filter only when the test needs the outer element too.

More on trimming a stream lives in filtering in streams.

7. The Primitive flatMap Variants

Besides the main method, Stream offers three primitive variants. They flatten and convert to a primitive stream in the same step.

7.1 Why They Exist

  • A Stream<Integer> stores boxed objects, and boxing costs memory plus time
  • IntStream, LongStream and DoubleStream hold raw values with no wrapper
  • Those primitive streams add handy methods such as sum, average, max and summaryStatistics
  • Going straight from nested objects to numbers skips a pointless boxing round trip

7.2 The flatMapToInt Method

IntStream flatMapToInt(Function<? super T, ? extends IntStream> mapper)

Parameter:
mapper - a stateless function applied to each element,
         which must return an IntStream

Returns: an IntStream holding the contents of every mapped stream

Here we count the letters in every word of every line. The mapper opens one line and reports the length of each word.

package com.javahandson.flatmap;

import java.util.Arrays;
import java.util.List;
import java.util.stream.IntStream;

public class FlatMapToIntExample {
    public static void main(String[] args) {

        List<String> lines = Arrays.asList("Java is fun", "Streams are powerful");

        IntStream lengths = lines.stream()
                .flatMapToInt(line -> Arrays.stream(line.split(" "))
                                            .mapToInt(String::length));

        System.out.println("Total letters: " + lengths.sum());
        // Output: Total letters: 27
    }
}

Look at the return type of the mapper. It hands back an IntStream, not a Stream<Integer>. Returning the boxed version fails to compile.

7.3 The flatMapToLong Method

LongStream flatMapToLong(Function<? super T, ? extends LongStream> mapper)

Parameter:
mapper - a stateless function applied to each element,
         which must return a LongStream

Returns: a LongStream holding the contents of every mapped stream

Reach for long when the totals grow large. Millisecond timings, byte counts and money in the smallest unit all overflow an int faster than you expect.

List<long[]> readings = Arrays.asList(
        new long[]{1200L, 4500L},
        new long[]{3000L});

LongStream all = readings.stream().flatMapToLong(Arrays::stream);

System.out.println("Total milliseconds: " + all.sum());
// Output: Total milliseconds: 8700

Notice the neat trick. Arrays.stream(long[]) already returns a LongStream, so the method reference fits the mapper with no extra work.

7.4 The flatMapToDouble Method

DoubleStream flatMapToDouble(Function<? super T, ? extends DoubleStream> mapper)

Parameter:
mapper - a stateless function applied to each element,
         which must return a DoubleStream

Returns: a DoubleStream holding the contents of every mapped stream
List<double[]> scores = Arrays.asList(
        new double[]{8.5, 9.0},
        new double[]{7.5, 6.0});

DoubleStream marks = scores.stream().flatMapToDouble(Arrays::stream);

System.out.println("Average score: " + marks.average().getAsDouble());
// Output: Average score: 7.75

The average method returns an OptionalDouble, because an empty stream has no average to give. Calling getAsDouble on empty data throws, so check first in real code.

7.5 flatMap Inside a Primitive Stream

The primitive streams carry their own flatMap too. An IntStream flatMap takes an int and returns another IntStream.

IntStream.of(1, 2, 3)
        .flatMap(n -> IntStream.of(n, n * 10))
        .forEach(n -> System.out.print(n + " "));
// Output: 1 10 2 20 3 30

Every input value expands into two output values. Notice there is no flatMapToObj anywhere, so to leave the primitive world you use boxed or mapToObj instead.

8. How flatMap Behaves Under the Hood

Four behaviours explain almost every surprise people hit with this method.

8.1 It Stays Lazy

Stream<String> pipeline = Stream.of(Arrays.asList("a", "b"))
        .flatMap(list -> {
            System.out.println("mapper running");
            return list.stream();
        });

System.out.println("nothing printed yet");
pipeline.forEach(System.out::println);
// Output:
// nothing printed yet
// mapper running
// a
// b

The mapper waits for a terminal operation. Build a pipeline and walk away, and your lambda never runs at all.

8.2 Every Inner Stream Closes Itself

The javadoc promises something useful here. Java closes each mapped stream once it has copied the contents across.

That matters for streams backed by a resource. Files.lines(path) holds an open file handle, so this pipeline reads many files without leaking them.

List<String> allLines = paths.stream()
        .flatMap(path -> {
            try {
                return Files.lines(path);   // closed for you by flatMap
            } catch (IOException e) {
                throw new UncheckedIOException(e);
            }
        })
        .collect(Collectors.toList());

The outer stream still needs your care. Only the inner streams get that free cleanup.

8.3 Short-Circuiting Before Java 10

Java 8 has a known wrinkle. A flatMap stage pulls a whole inner stream even when findFirst, limit or anyMatch already had enough.

// On Java 8 this never finishes: the infinite inner stream
// keeps producing values even though limit(2) wants only two.
Stream.of(1, 2)
        .flatMap(n -> Stream.iterate(n, i -> i + 1))
        .limit(2)
        .forEach(System.out::println);

Java 10 fixed the underlying bug, so the same code stops early there. On Java 8, cap the inner stream yourself with a limit inside the mapper.

8.4 flatMap and Parallel Streams

Parallel pipelines split the outer stream across threads. Each inner stream, however, gets walked sequentially by the thread that opened it.

  • Many outer elements with small inner collections parallelise well
  • Two outer elements with huge inner collections barely parallelise at all
  • Flatten first, then go parallel, when the inner data does the heavy lifting
  • Always measure before assuming parallel wins

The trade-offs get a fuller treatment in parallel stream in Java 8.

9. flatMap Outside the Stream Interface

The name shows up in two other places worth knowing about.

9.1 Optional.flatMap

An Optional is a container holding zero or one value. Its flatMap follows the same rule: your function returns another Optional, and Java unwraps one layer for you.

Optional<String> name = Optional.of("riya");

// map would give Optional<Optional<String>>
Optional<String> upper = name.flatMap(n -> Optional.of(n.toUpperCase()));

System.out.println(upper.get());
// Output: RIYA

Use it when the method you call already returns an Optional. That keeps nested optionals out of your code.

9.2 mapMulti in Newer Java

Java 16 added Stream.mapMulti. Instead of returning a stream, your lambda pushes values into a consumer, which avoids creating a small stream object per element.

// Java 16 and later
List<String> flat = nested.stream()
        .<String>mapMulti((list, consumer) -> list.forEach(consumer))
        .collect(Collectors.toList());

Stick with flatMap as your default. Consider mapMulti only on a hot path where profiling shows those tiny streams hurting.

10. The flatMap Family Side by Side

MethodMapper must returnYou get backReach for it when
flatMapStream<R>Stream<R>Flattening objects, lists or arrays
flatMapToIntIntStreamIntStreamCounts, lengths, whole numbers
flatMapToLongLongStreamLongStreamTimings, big totals, ids
flatMapToDoubleDoubleStreamDoubleStreamPrices, scores, averages
IntStream.flatMapIntStreamIntStreamExpanding one int into several

One shape covers all five rows. Whatever kind of stream you want out, your mapper must return that same kind of stream.

11. Common Mistakes and Pitfalls

11.1 Reaching for map When You Need flatMap

// Wrong: you wanted subjects, you got lists of subjects
Stream<List<String>> wrong = students.stream().map(Student::getSubjects);

// Right
Stream<String> right = students.stream().flatMap(s -> s.getSubjects().stream());

The compiler usually catches this at the collect step with a confusing message about incompatible types. Read the generic type in the error and count the layers.

11.2 Returning a Collection Instead of a Stream

// Does not compile: List is not a Stream
students.stream().flatMap(Student::getSubjects);

// Compiles: add .stream() on the returned list
students.stream().flatMap(s -> s.getSubjects().stream());

Every beginner writes the broken line once. The mapper must return a stream, so append .stream() to whatever collection you have.

11.3 Returning null From the Mapper

The JDK treats a null mapped stream as an empty one, so you escape a NullPointerException. Do not rely on that anyway.

// Works, but hides your intent
.flatMap(s -> s.getSubjects() == null ? null : s.getSubjects().stream())

// Clear and safe
.flatMap(s -> s.getSubjects() == null
        ? Stream.empty()
        : s.getSubjects().stream())

Returning Stream.empty() says “this element contributes nothing” out loud. The next reader thanks you.

11.4 Arrays.stream on a Primitive Array

int[][] matrix = {{1, 2}, {3, 4}};

// Does not compile: Arrays.stream(int[]) gives an IntStream,
// and flatMap wants a Stream
Arrays.stream(matrix).flatMap(Arrays::stream);

// Correct
IntStream flat = Arrays.stream(matrix).flatMapToInt(Arrays::stream);
System.out.println(flat.sum());
// Output: 10

The same code works fine for String[][] and breaks for int[][]. That inconsistency catches people out, so remember which overload of Arrays.stream you called.

11.5 One flatMap Only Removes One Layer

List<List<List<String>>> deep = Arrays.asList(
        Arrays.asList(Arrays.asList("a", "b"), Arrays.asList("c")),
        Arrays.asList(Arrays.asList("d")));

List<String> flat = deep.stream()
        .flatMap(List::stream)   // peels layer one
        .flatMap(List::stream)   // peels layer two
        .collect(Collectors.toList());

System.out.println(flat);
// Output: [a, b, c, d]

Three layers deep means two flatMap calls. Count your angle brackets and you will know how many you need.

11.6 Side Effects Inside the Mapper

The javadoc asks for a stateless mapper. Adding to an outside list from inside your lambda breaks on parallel streams and confuses readers on sequential ones.

  • Never write to a shared ArrayList from the mapper
  • Never mutate the objects you are streaming over
  • Let collect build the result instead
  • Keep counters out; use count or a collector

11.7 Forgetting the Terminal Operation

// Nothing happens at all
students.stream().flatMap(s -> s.getSubjects().stream());

// Now the work runs
List<String> subjects = students.stream()
        .flatMap(s -> s.getSubjects().stream())
        .collect(Collectors.toList());

Also remember the one-shot rule. Once a terminal operation runs, that stream retires, and touching it again throws IllegalStateException.

12. Practical Walkthrough: A Small Order Report

Let us tie the ideas together. We have orders, each holding line items, and we want three numbers out of them.

12.1 The Data

package com.javahandson.flatmap;

import java.util.*;
import java.util.stream.Collectors;

class Item {
    private final String name;
    private final double price;
    private final int qty;

    Item(String name, double price, int qty) {
        this.name = name;
        this.price = price;
        this.qty = qty;
    }

    public String getName() { return name; }
    public double getPrice() { return price; }
    public int getQty() { return qty; }
}

class Order {
    private final String customer;
    private final List<Item> items;

    Order(String customer, List<Item> items) {
        this.customer = customer;
        this.items = items;
    }

    public List<Item> getItems() { return items; }
}

12.2 Building the Report

public class OrderReport {
    public static void main(String[] args) {

        List<Order> orders = Arrays.asList(
                new Order("Riya", Arrays.asList(
                        new Item("Keyboard", 1200.0, 1),
                        new Item("Mouse", 600.0, 2))),
                new Order("Sam", Arrays.asList(
                        new Item("Monitor", 9000.0, 1))),
                new Order("Neha", Arrays.asList(
                        new Item("Mouse", 600.0, 1),
                        new Item("Keyboard", 1200.0, 1),
                        new Item("Cable", 150.0, 3))));

        List<String> products = orders.stream()
                .flatMap(order -> order.getItems().stream())
                .map(Item::getName)
                .distinct()
                .sorted()
                .collect(Collectors.toList());

        double revenue = orders.stream()
                .flatMap(order -> order.getItems().stream())
                .mapToDouble(item -> item.getPrice() * item.getQty())
                .sum();

        Map<String, Integer> unitsPerProduct = orders.stream()
                .flatMap(order -> order.getItems().stream())
                .collect(Collectors.groupingBy(
                        Item::getName,
                        TreeMap::new,
                        Collectors.summingInt(Item::getQty)));

        System.out.println("Products ordered: " + products);
        System.out.println("Total revenue: " + revenue);
        System.out.println("Units per product: " + unitsPerProduct);
    }
}
Output:
Products ordered: [Cable, Keyboard, Monitor, Mouse]
Total revenue: 13650.0
Units per product: {Cable=3, Keyboard=2, Monitor=1, Mouse=3}

12.3 Reading the Output

  • All three pipelines open with the same flatMap, which turns three orders into six line items
  • The product list then maps to names, drops duplicates and sorts them
  • Revenue multiplies price by quantity, and mapToDouble hands us a free sum
  • The last pipeline groups by product name and totals the quantities per group
  • Passing TreeMap::new keeps the keys in a predictable order

Without flatMap, each of these would need a loop inside a loop. Flattening once at the top made every later step a one-liner. Collectors get their own deep dive in introduction to the Collectors class.

13. Interview Questions on flatMap in Java 8 Streams

Q: What is flatMap in Java 8 streams?

A: The flatMap method is an intermediate operation on Stream. It applies a function that turns each element into a stream, then merges all those streams into one flat stream. Use it to remove one layer of nesting from your data.

Q: What is the difference between map and flatMap in Java 8?

A: The map method turns one element into exactly one element, so the count never changes. The flatMap method turns one element into a stream of values, so the count can grow or shrink. Only flatMap removes nesting.

Q: When should I use flatMap instead of map?

A: Use flatMap when your lambda produces a collection, an array or another stream. If your pipeline shows a type like Stream of List of String and you wanted plain strings, flatMap fixes it.

Q: Can flatMap return a List instead of a Stream?

A: No. The mapper must return a Stream, so the code fails to compile with a List. Simply call .stream() on the list inside your lambda.

Q: How do I flatten a list of lists in Java 8?

A: Call nested.stream().flatMap(List::stream) and then collect the result. That single call pours every inner list into one stream of elements.

Q: How do I flatten a 2D array with flatMap?

A: For an object array such as String[][], use Arrays.stream(grid).flatMap(Arrays::stream). For a primitive array such as int[][], switch to flatMapToInt, because Arrays.stream on an int[] returns an IntStream.

Q: Can flatMap remove elements from a stream?

A: Yes. Return Stream.empty() from the mapper and that element contributes nothing to the output. This lets you validate and convert values in a single pass.

Q: What happens if my flatMap mapper returns null?

A: The JDK substitutes an empty stream, so your program survives without a NullPointerException. Still return Stream.empty() yourself, because it states your intent clearly.

Q: What are flatMapToInt, flatMapToLong and flatMapToDouble?

A: They flatten a nested structure and hand you a primitive stream in one step. Each one needs a mapper that returns IntStream, LongStream or DoubleStream, and each gives you methods such as sum, average and max.

Q: Does flatMap change my original collection?

A: No. Streams never write back to their source. The flatMap method builds a fresh stream and leaves every list, array and object exactly as it found them.

Q: How many flatMap calls do I need for deeply nested data?

A: One per layer you want to peel. A list of lists needs one call, and a list of lists of lists needs two.

Q: Is flatMap a terminal operation?

A: No, it is an intermediate operation. It returns a Stream, stays lazy, and runs only when a terminal operation such as collect, forEach or count asks for values.

14. Conclusion

Let us wrap up what we covered. The flatMap in Java 8 streams exists for one reason: to remove a layer of nesting from your data.

You give it a function that returns a stream. Java runs that function on every element and pours the results into one flat stream. Lists inside lists, arrays inside arrays and collections inside objects all yield to the same two-line pattern.

We saw the contrast with map, which always gives one output per input. Empty streams turned out to be a tidy way to drop unwanted elements mid-pipeline. The primitive variants flatMapToInt, flatMapToLong and flatMapToDouble flatten and convert to numbers in a single step.

We also looked past the happy path: laziness, the automatic closing of inner streams, the Java 8 short-circuiting wrinkle and the limits of parallel work. Finally we walked through seven mistakes and one small report program.

Try the examples in your own IDE. Change the data, break the pipelines on purpose, and read the compiler errors. Nothing teaches the type layers faster than that.

Further Reading

Leave a Comment