Collecting Results

Choose between toList, Collectors.toMap, joining and teeing, and avoid the duplicate-key and null traps that break collectors in production.

Syntaxstream.toList() stream.collect(Collectors.toMap(keyFn, valueFn, mergeFn, LinkedHashMap::new)) stream.collect(Collectors.joining(", ", "[", "]"))

A terminal operation decides what the pipeline produces. toList() covers most cases; collect(collector) covers the rest, with ready-made collectors in java.util.stream.Collectors.

Lists

CallResult
stream.toList() (Java 16)Unmodifiable, allows null elements
Collectors.toUnmodifiableList() (Java 10)Unmodifiable, throws on null
Collectors.toList()No promise about type or mutability
Collectors.toCollection(ArrayList::new)Exactly the collection you asked for

Use toList() by default. When code later adds to the list, ask for an ArrayList with toCollection rather than relying on what Collectors.toList() happens to return today.

Maps

Map<String, Product> bySku = products.stream()
    .collect(Collectors.toMap(Product::sku, Function.identity()));

toMap is the usual way to index records by id before matching them against another list. Two traps catch people in production:

  • Duplicate keys throw IllegalStateException. Data that was unique in testing is often not unique in a real table (two accounts whose emails match once you lowercase them for the key, a join that repeats rows). Pass a merge function such as (first, second) -> second when duplicates are possible, and make the choice deliberately.
  • Null values throw NullPointerException in every form of toMap, even when the target is a HashMap that would accept null. Filter them out or map them to a default first.

The four-argument form takes a map factory. LinkedHashMap::new keeps encounter order and TreeMap::new sorts by key, which matters when the map is serialised to JSON or printed in a log and you want stable output.

Strings and two results at once

Collectors.joining(", ") builds a string with a delimiter, and an optional prefix and suffix. It is the right tool for log messages and CSV lines; do not concatenate with reduce. Collectors.teeing (Java 12) runs two collectors over the same elements and combines the results, for example a count and a total for a summary response, in one pass.

Example

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

When to use it

  • Index products by SKU with toMap so an order import can look each one up in constant time.
  • Build a comma-separated list of missing fields for a validation error message with joining.
  • Return the count and total of a customer's invoices in one summary object with teeing.

More examples

Duplicate keys in toMap

The plain toMap fails on the repeated email. With a merge function the later row wins, and LinkedHashMap keeps the keys in the order they first appeared.

Example · java
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;

record Row(String email, long id) {}

void main() {
    List<Row> rows = List.of(
        new Row("[email protected]", 1),
        new Row("[email protected]", 2),
        new Row("[email protected]", 3));

    try {
        rows.stream().collect(Collectors.toMap(Row::email, Row::id));
    } catch (IllegalStateException e) {
        IO.println(e.getMessage());
    }

    Map<String, Long> latestId = rows.stream()
        .collect(Collectors.toMap(Row::email, Row::id, (first, second) -> second, LinkedHashMap::new));
    IO.println(latestId);
}
// Output:
// Duplicate key [email protected] (attempted merging values 1 and 3)
// {[email protected]=3, [email protected]=2}

Which list do you get?

toCollection(ArrayList::new) guarantees a list you can add to. toUnmodifiableList refuses null elements while Stream.toList accepts them, which is easy to miss when switching between the two.

Example · java
import java.util.ArrayList;
import java.util.List;
import java.util.stream.Collectors;
import java.util.stream.Stream;

void main() {
    List<String> growable = Stream.of("b", "a")
        .collect(Collectors.toCollection(ArrayList::new));
    growable.add("c");
    IO.println(growable);

    try {
        Stream.of("a", null).collect(Collectors.toUnmodifiableList());
    } catch (NullPointerException e) {
        IO.println("toUnmodifiableList rejects null");
    }

    IO.println(Stream.of("a", null).toList());
}
// Output:
// [b, a, c]
// toUnmodifiableList rejects null
// [a, null]

Two results in one pass with teeing

teeing feeds every element to both collectors and passes the two results to Summary::new. It suits summary endpoints that need several figures from one query result.

Example · java
import java.util.List;
import java.util.stream.Collectors;

record Summary(long count, int totalCents) {}

void main() {
    List<Integer> amounts = List.of(1200, 450, 3000);

    Summary summary = amounts.stream()
        .collect(Collectors.teeing(
            Collectors.counting(),
            Collectors.summingInt(Integer::intValue),
            Summary::new));

    IO.println(summary);
}
// Output:
// Summary[count=3, totalCents=4650]

Discussion

  • Be the first to comment on this lesson.