Collecting Results
Choose between toList, Collectors.toMap, joining and teeing, and avoid the duplicate-key and null traps that break collectors in production.
stream.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
| Call | Result |
|---|---|
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) -> secondwhen duplicates are possible, and make the choice deliberately. - Null values throw
NullPointerExceptionin every form oftoMap, even when the target is aHashMapthat would acceptnull. 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
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.
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.
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.
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