JAVA / STREAMS, LAMBDAS AND OPTIONAL
Filtering, mapping and flattening with flatMap
Turn nested collections into one flat stream with flatMap, and decide when a stage needs filter, map or flatMap.
What you will learn
- Use map for one-to-one work and flatMap when one element yields many or none
- Spot Stream<List<T>> as the sign that a map stage should have been flatMap
- Return Stream.empty() from a flatMap mapper to drop an element with no filter stage
- Place filter before flatMap for container tests, after it for element tests
Understanding Filtering, mapping and flattening with flatMap
filter and map both keep the pipeline exactly one element wide. filter decides keep or drop and never changes the element type; map swaps each element for exactly one replacement and may change the type. Neither can turn one element into three, so when each order carries a list of items, map(Order::items) hands you a stream whose elements are lists, not strings. flatMap exists for that case: its mapper returns a stream, and flatMap pushes the elements of that stream downstream instead of the stream object itself.
Because flatMap forwards the contents of each inner stream, it removes exactly one level of nesting per call, so a Stream<List<List<String>>> needs two flatMap stages before you reach the strings. The inner stream may be empty, in which case that input contributes nothing, or hold twenty elements, in which case one input becomes twenty outputs. That is why count() after a flatMap has no relation to the source size, and why flatMap alone can already do the work of filter (return Stream.empty()) and map (return Stream.of(one)).
Stage order matters because filter only ever sees what the previous stage produced. .filter(o -> o.items().contains("mouse")).flatMap(o -> o.items().stream()) picks whole orders and then emits every item they contain, while .flatMap(o -> o.items().stream()).filter(i -> i.startsWith("m")) picks items. Ask which level your condition talks about and put the filter on that side of the flatMap. The mapper itself runs lazily, once per element as the terminal stage pulls, so it must build a fresh inner stream each time.
flatMap replaces each element with the contents of a stream instead of the stream itself, removing exactly one level of nesting.
import java.util.List;
public class FlattenOrders {
record Order(String customer, List<String> items) {}
public static void main(String[] args) {
List<Order> orders = List.of(
new Order("ana", List.of("keyboard", "mouse")),
new Order("bo", List.of()),
new Order("cy", List.of("monitor", "mouse", "cable")));
List<List<String>> nested = orders.stream()
.map(Order::items)
.toList();
List<String> allItems = orders.stream()
.flatMap(order -> order.items().stream())
.toList();
List<String> mItems = orders.stream()
.flatMap(order -> order.items().stream())
.filter(item -> item.startsWith("m"))
.toList();
List<String> itemsOfBigOrders = orders.stream()
.filter(order -> order.items().size() > 2)
.flatMap(order -> order.items().stream())
.toList();
System.out.println("map: " + nested);
System.out.println("flatMap: " + allItems);
System.out.println("filter after flatMap: " + mItems);
System.out.println("filter before flatMap: " + itemsOfBigOrders);
}
}flatMap replaces each element with the contents of a stream rather than the stream object, so it removes exactly one level of nesting and breaks the one-in-one-out rule that map and filter obey.
Worked examples
A mapper that returns zero or one element
Uses flatMap to parse text and silently drop the inputs that are not numbers.
import java.util.List;
import java.util.stream.Stream;
public class ParseFlatMap {
static Stream<Integer> parseOrSkip(String text) {
try {
return Stream.of(Integer.valueOf(text));
} catch (NumberFormatException e) {
return Stream.empty();
}
}
public static void main(String[] args) {
List<String> raw = List.of("10", "x", "7", "", "25");
List<Integer> numbers = raw.stream()
.flatMap(ParseFlatMap::parseOrSkip)
.toList();
System.out.println(numbers);
System.out.println("kept " + numbers.size() + " of " + raw.size());
}
}Example explained
Line 1parseOrSkip returns a one-element stream on success and an empty stream on failure, so the mapper itself decides how many outputs each input produces.
Line 2flatMap splices those streams together, which removes "x" and "" without any separate filter stage.
Line 3The declared return type Stream<Integer> is what gives Stream.empty() its element type; written inline as a lambda you would often need Stream.<Integer>empty().
Line 4toList() ends up with three elements although the source had five, because flatMap is not size-preserving.
Pairs from two lists
Nests a map inside a flatMap to combine every size with every colour.
import java.util.List;
public class Variants {
public static void main(String[] args) {
List<String> sizes = List.of("S", "M");
List<String> colours = List.of("red", "blue", "green");
List<String> variants = sizes.stream()
.flatMap(size -> colours.stream().map(colour -> size + "-" + colour))
.toList();
variants.forEach(System.out::println);
System.out.println(variants.size() + " variants from "
+ sizes.size() + " x " + colours.size());
}
}Example explained
Line 1colours.stream().map(...) builds a fresh three-element stream for each size, so the mapper runs twice and hands back two inner streams.
Line 2flatMap drains one inner stream completely before asking for the next, which is why the outer value varies slowly and the inner value varies fast.
Line 3The inner stage is a plain map because one colour produces exactly one variant string; only the outer stage needs flattening.
Line 4Using map instead of flatMap at the outer level would produce List<Stream<String>>, which does not compile against the declared List<String>.
Flattening away the empty Optionals
Turns a Stream<Optional<String>> into a Stream<String> with Optional::stream.
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
public class OptionalFlatten {
public static void main(String[] args) {
Map<String, String> extensions = Map.of("ana", "4021", "cy", "4093");
List<String> team = List.of("ana", "bo", "cy");
List<String> viaOptional = team.stream()
.map(name -> Optional.ofNullable(extensions.get(name)))
.flatMap(Optional::stream)
.toList();
List<String> viaNullCheck = team.stream()
.map(extensions::get)
.filter(Objects::nonNull)
.toList();
System.out.println(viaOptional);
System.out.println(viaNullCheck);
System.out.println(viaOptional.equals(viaNullCheck));
}
}Example explained
Line 1Optional.stream() yields a stream of zero or one element, so it is exactly the shape flatMap wants and the missing entry for "bo" disappears.
Line 2The map stage stays one-to-one, three names in and three Optionals out; the flatMap stage is what reduces the count to two.
Line 3Order comes from team, not from the map, so the result is deterministic even though Map.of has no defined iteration order.
Line 4Both pipelines agree, but the Optional version never lets a null value travel through the stream.
Important notes
flatMap consumes and closes each stream the mapper returns, so create a new one per element; returning one stored Stream twice throws IllegalStateException: stream has already been operated upon or closed.
Stream.flatMap documents a null inner stream as being treated like an empty one, but Optional.flatMap throws NullPointerException when its mapper returns null, so return Stream.empty() or Optional.empty() rather than null.
Common mistakes
Passing the collection instead of a stream: .flatMap(Order::items) is rejected at compile time with a bad return type error, because List<String> is not a Stream; the mapper needs order.items().stream().
Filtering at the wrong level: .filter(o -> o.items().contains("mouse")).flatMap(o -> o.items().stream()) emits keyboard and cable as well, since the predicate selected whole orders rather than items.
Assuming one flatMap flattens all the way down: with List<List<List<String>>> a single flatMap still leaves Stream<List<String>>, so the next stage sees lists and calls such as length() do not compile.
Try it yourself
Change, predict, then run
Starting from List<String> lines = List.of("the quick brown fox", "jumps over", "the lazy dog"), use flatMap with Arrays.stream(line.split(" ")) to build a single stream of words, keep only those with four or more letters, and print one per line. Then write a second version whose mapper returns Stream.empty() for lines shorter than eleven characters, and compare the two word counts.
Open the Java workspaceCheck your understanding
The same mapper lambda s -> Arrays.stream(s.split(" ")) is used once with map and once with flatMap on a Stream<String> of text lines. What is the difference in the resulting stream?
- Nothing differs; flatMap is just an alias for map used when the mapper already returns a stream.
- map produces a Stream<String> of words, while flatMap produces a Stream<Stream<String>> of per-line streams.
- map produces a Stream<Stream<String>> with one element per line, while flatMap produces a Stream<String> with one element per word.
- Both produce a Stream<String> of words, but flatMap also removes duplicate words.
Show answer
map keeps whatever the function returns as a single element, so the element type becomes the function's return type and the count still matches the number of lines. flatMap instead drains each returned stream and forwards its elements, so the count becomes the number of words. Option 3 is tempting because flatMap is described as collapsing, but collapsing a level of nesting is not deduplication; only distinct() removes duplicates.