flatMap

Flatten nested lists, optional lookups and split strings into one stream with flatMap, and know when mapMulti is the simpler choice.

Syntaxstream.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-separated roles column or a multi-value query parameter becomes one stream of individual values with flatMap(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, without isPresent and get.

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 null from the flatMap function. A null stream is treated as empty, but returning Stream.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

Try it yourself
Loading editor…
Press Run to execute the code.

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.

Example · java
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.

Example · java
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.

Example · java
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

  • Be the first to comment on this lesson.