The labs on this site need JavaScript to run. The same content is written out below.
Learn › Java › Lambdas and streams
Collecting a stream
Into what shape do you collect the result of a stream?
The elements that come out of a stream must be received at the end by a terminal operation such as collect before you can use them. Depending on which Collector you give it, the result is a List, a Map grouped by key, or a single string.
Checked against real execution · Checked by running 24 combinations on JDK 21
Concepts in this lab
- Stream API
- This API expresses filtering, transformation, and aggregation over data such as collections in a declarative way. You chain intermediate operations such as filter and map, and finish with a terminal operation such as collect or forEach. Intermediate operations do not run until the terminal operation is called (lazy evaluation), and a stream does not modify its source data and can be used only once.
- Collector and collect
- collect is the terminal operation that gathers the elements that come out of a stream into a single result, and a Collector is the object that represents how they are gathered (which container is used and how the elements are put in). The Collectors class provides the Collectors that are used often. toList and toSet put the elements into a collection, joining concatenates them into a single string, and toMap puts them into a Map with the key and the value that you specify. toMap throws an IllegalStateException when the same key appears twice, so if keys can be duplicated, give a merge function that combines the two values as the third argument.
- groupingBy and partitioningBy
- These are Collectors that divide the elements of a stream into groups and collect them into a Map. groupingBy computes a key for each element and groups the elements that have the same key, and its default result is a Map<key, List<element>>. If you give a Collector such as counting or summingInt as the second argument, you get the count or the sum of each group instead of a list. partitioningBy divides the elements into two groups, a true group and a false group, by a single condition, and the two keys are always present even when no element belongs to a group. The Map that groupingBy returns does not guarantee an order, so if you need an order, specify separately that a Map such as TreeMap is to be used.
- Method reference
- This is a syntax that refers to an existing method by name in place of a lambda. You write it by joining a class (or an object) and a method name with ::, as in String::length or System.out::println. It is a shorter way to write a lambda that does nothing but call one method.
- List, Set, Map
- These are the three basic kinds of Java collections. A List is ordered, allows duplicates, and is accessed by index. A Set does not allow duplicates. A Map stores key-value pairs, and its keys cannot be duplicated. You choose among them by the nature of the data (whether order is needed, whether duplicates are allowed, and whether you look things up by key).
Try these
- Default settings — The same customer appears twice in toMap. An IllegalStateException is thrown.
- Supplying a merge function — The values of the same key are added up into one.
- Collecting with toList — The elements go into a List in the order in which they came out.
- Grouping with groupingBy — A list of orders is built for each customer.
- Grouping and counting — Only the count is kept instead of the list. The type of the values is Long.
- Splitting into two with partitioningBy — The elements are split into those for which the condition is true and those for which it is false.
- Splitting into two when there are no orders — Even with no elements, the two entries, false and true, are always present.
Running with the default settings
- orders.stream(): This creates a stream from which the 5 orders come out one at a time, in the order kim:300, han:100, kim:200, choi:400, han:500. Nothing has been collected yet.
- .collect(Collectors.toMap(Order::customer, Order::price)): collect is a terminal operation that receives the elements one at a time as they come out and puts them into a container, and the Collector in the parentheses decides which container is used and how the elements are put in. toMap prepares an empty Map. For each element, it puts in customer as the key and price as the value. A key can have only one value.
- Order kim:300: The key kim and the value 300 are put into the Map.
- Order han:100: The key han and the value 100 are put into the Map.
- Order kim:200: The same key (kim) is already in the Map. The code does not say which to keep, the existing value 300 or the new value 200, so an IllegalStateException is thrown and the program stops here. The exception message is "Duplicate key kim (attempted merging values 300 and 200)".
Result of this run
- toMap holds only one value per key. If the data can have duplicate keys, you must either supply a merge function as the third argument or use groupingBy, which collects the duplicates into a list.
Code
record Order(String customer, int price) {
@Override
public String toString() { return customer + ":" + price; } // to keep the output short
}
List<Order> orders = List.of(
new Order("kim", 300), new Order("han", 100), new Order("kim", 200),
new Order("choi", 400), new Order("han", 500));
Map<String, Integer> result = orders.stream()
.collect(Collectors.toMap(Order::customer, Order::price));
System.out.println(result);
Assumptions of this model
The output and the exceptions (kind and message) of the 24 combinations were checked against the result of running the same code on JDK 21 on this PC. The type of the result was confirmed by whether code that declares the result with that type actually compiles, and it was also confirmed that assigning the result of counting() to a Map<String, Integer> does not compile. The Map that groupingBy and toMap return does not guarantee an order (on JDK 21 it is a HashMap). This lab shows a Map sorted in lexicographic order of the keys, and for the customer names used here, the actual output order on JDK 21 was the same as that. With other names, the actual output order can differ. The wording of the exception message can differ between JDK versions. The lab covers only the case in which a stream handles its elements one at a time in order (a stream that is not parallel).
Previous lab: Lazy evaluation in streams · Next lab: Parallel streams and the common pool