flatmap in Java 8 streams
-
Last Updated: March 10, 2024
-
By: javahandson
-
Series

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.
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.
flatMap signature, and how to read those scary genericsmap and flatMap differ, side by sideflatMapToInt, flatMapToLong and flatMapToDoubleFlattening removes one layer of nesting. That is the whole idea. Everything else in this article follows from that one sentence.
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>.
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.
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.
Stream, so you can keep chainingThat 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.
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.
<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 streamThe 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.
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.
Stream<String> holding “Learning” and “Java”String[], so we hold Stream<String[]>Arrays::stream turns each array into a small stream, and those merge into one Stream<String>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.
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.
Nested data shows up in many disguises. Here are the five you will meet most often, each with the pipeline that flattens it.
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.
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.
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.
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.
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.
This comparison is the single most asked interview question about either method, so let us make it crisp.
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]| Point | map | flatMap |
|---|---|---|
| Mapper returns | A single value | A Stream of values |
| Elements out | Always the same count | Zero, one or many per element |
| Nesting | Keeps it | Removes one layer |
| Typical input | Stream<Employee> | Stream<List<Employee>> |
| Typical output | Stream<String> | Stream<Employee> |
| Common use | Pull one field out | Merge inner collections |
| Can drop elements | No | Yes, via an empty stream |
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.
Most tutorials stop at flattening. But because your mapper returns a stream, that stream can be empty, and an empty stream contributes nothing.
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.
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.
Besides the main method, Stream offers three primitive variants. They flatten and convert to a primitive stream in the same step.
Stream<Integer> stores boxed objects, and boxing costs memory plus timeIntStream, LongStream and DoubleStream hold raw values with no wrappersum, average, max and summaryStatisticsIntStream 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 streamHere 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.
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 streamReach 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: 8700Notice the neat trick. Arrays.stream(long[]) already returns a LongStream, so the method reference fits the mapper with no extra work.
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 streamList<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.75The 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.
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 30Every 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.
Four behaviours explain almost every surprise people hit with this method.
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
// bThe mapper waits for a terminal operation. Build a pipeline and walk away, and your lambda never runs at all.
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.
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.
Parallel pipelines split the outer stream across threads. Each inner stream, however, gets walked sequentially by the thread that opened it.
The trade-offs get a fuller treatment in parallel stream in Java 8.
The name shows up in two other places worth knowing about.
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: RIYAUse it when the method you call already returns an Optional. That keeps nested optionals out of your code.
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.
| Method | Mapper must return | You get back | Reach for it when |
|---|---|---|---|
| flatMap | Stream<R> | Stream<R> | Flattening objects, lists or arrays |
| flatMapToInt | IntStream | IntStream | Counts, lengths, whole numbers |
| flatMapToLong | LongStream | LongStream | Timings, big totals, ids |
| flatMapToDouble | DoubleStream | DoubleStream | Prices, scores, averages |
| IntStream.flatMap | IntStream | IntStream | Expanding 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.
// 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.
// 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.
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.
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: 10The same code works fine for String[][] and breaks for int[][]. That inconsistency catches people out, so remember which overload of Arrays.stream you called.
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.
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.
ArrayList from the mappercollect build the result insteadcount or a collector// 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.
Let us tie the ideas together. We have orders, each holding line items, and we want three numbers out of them.
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; }
}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}mapToDouble hands us a free sumTreeMap::new keeps the keys in a predictable orderWithout 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.
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.
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.
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.
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.
A: Call nested.stream().flatMap(List::stream) and then collect the result. That single call pours every inner list into one stream of elements.
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.
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.
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.
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.
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.
A: One per layer you want to peel. A list of lists needs one call, and a list of lists of lists needs two.
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.
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.