flatMap
Flatten nested lists, optional lookups and split strings into one stream with flatMap, and know when mapMulti is the simpler choice.
stream.flatMap(x -> x.children().stream())
stream.flatMap(Optional::stream)
stream.<R>mapMulti((x, sink) -> sink.accept(r))map turns each element into exactly one new element. flatMap turns each element into a stream of zero, one or many elements and joins all those streams into one. Use it whenever a single input produces several outputs, or possibly none.
List<OrderLine> allLines = orders.stream()
.flatMap(order -> order.lines().stream())
.toList();With map(Order::lines) you would get a Stream<List<OrderLine>>, a stream of lists. flatMap removes that level of nesting so the next step sees individual lines.
Where it appears in back-end code
- One-to-many relations. Orders and their lines, users and their roles, posts and their tags. Flatten, then group or total across all children.
- Splitting values. A header such as
Accept-Language, a comma-separatedrolescolumn or a multi-value query parameter becomes one stream of individual values withflatMap(s -> Arrays.stream(s.split(","))). - Optional lookups. When a method returns
Optional,flatMap(Optional::stream)(Java 9) keeps the found values and drops the empty ones, withoutisPresentandget.
mapMulti
mapMulti (Java 16) does the same job in a push style: you receive each element and a consumer, and call accept once per output. It avoids creating a small stream for every element, and it reads well when the outputs come from a loop or an if. Java usually needs a type witness, as in .<String>mapMulti(...), to know what you are emitting.
Mistakes people make
- Returning
nullfrom theflatMapfunction. Anullstream is treated as empty, but returningStream.empty()says so and works the same everywhere. - Flattening a lazy relation loaded from a database. Calling
order.lines()on an entity for each order can run one query per order, the N+1 problem. Fetch the children in one query, then flatten in memory. - Losing the parent. After flattening, each line no longer knows which order it came from. If you need it, map to a pair such as a small record holding both before or while flattening.
Example
When to use it
- Total quantities per SKU across all lines of today's orders for a warehouse picking list.
- Collect the distinct roles of a user from a comma-separated column before an authorisation check.
- Resolve a batch of ids through a cache that returns Optional and keep only the hits.
More examples
Dropping failed lookups with Optional::stream
Each lookup returns an Optional, and Optional::stream turns a present value into a one-element stream and an empty one into nothing. Id 2 simply disappears.
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.stream.Stream;
static final Map<Long, String> EMAILS = Map.of(1L, "[email protected]", 3L, "[email protected]");
static Optional<String> findEmail(long id) {
return Optional.ofNullable(EMAILS.get(id));
}
void main() {
List<String> found = Stream.of(1L, 2L, 3L)
.map(id -> findEmail(id))
.flatMap(Optional::stream)
.toList();
IO.println(found);
}
// Output:
// [[email protected], [email protected]]Splitting a comma-separated column
Each row holds several roles, and flatMap turns them into one stream of single roles. strip removes the space after the comma before distinct compares them.
import java.util.Arrays;
import java.util.List;
void main() {
List<String> roleColumns = List.of("admin,editor", "viewer", "editor, viewer");
List<String> roles = roleColumns.stream()
.flatMap(column -> Arrays.stream(column.split(",")))
.map(String::strip)
.distinct()
.sorted()
.toList();
IO.println(roles);
}
// Output:
// [admin, editor, viewer]mapMulti for conditional output
The lambda pushes one label per parcel with sink.accept, and s-2 pushes nothing. The <String> type witness tells the compiler what the stream will contain.
import java.util.List;
record Shipment(String id, List<String> parcels) {}
void main() {
List<Shipment> shipments = List.of(
new Shipment("s-1", List.of("p1", "p2")),
new Shipment("s-2", List.of()),
new Shipment("s-3", List.of("p1")));
List<String> labels = shipments.stream()
.<String>mapMulti((shipment, sink) -> {
for (String parcel : shipment.parcels()) {
sink.accept(shipment.id() + "/" + parcel);
}
})
.toList();
IO.println(labels);
}
// Output:
// [s-1/p1, s-1/p2, s-3/p1]
Discussion