We have used the collect() method several times before to combine the elements returned by the Stream into an ArrayList. This is a reduce operation, which is useful for converting a collection into another type (usually a mutable collection). The collect() function, if used in combination with some methods in the Collectors tool class, can provide great convenience, as we will introduce in this section.
Let's continue to use the previous Person list as an example to see what the collect() method can do. Suppose we want to find all people older than 20 years old from the original list. Here is the version implemented using mutability and the forEach() method:
Copy the code code as follows:
List<Person> olderThan20 = new ArrayList<>(); people.stream()
.filter(person -> person.getAge() > 20)
.forEach(person -> olderThan20.add(person)); System.out.println("People older than 20: " + olderThan20);
We use the filter() method to filter out all people older than 20 from the list. Then, in the forEach method, we add elements to an ArrayList that has been initialized previously. Let’s take a look at the output of this code first, and then reconstruct it later.
Copy the code code as follows:
People older than 20: [Sara - 21, Jane - 21, Greg - 35]
The output of the program is correct, but there is still a little problem. First, adding elements to a collection is a low-level operation - it is imperative, not declarative. If we want to transform this iteration into concurrent, we have to consider thread safety issues - variability makes it difficult to parallelize. Fortunately, this problem can be easily solved using the collect() method. Let’s see how this is achieved.
The collect() method accepts a Stream and collects them into a result container. To do this, it needs to know three things:
+ How to create a result container (for example, using the ArrayList::new method) + How to add a single element to the container (for example, using the ArrayList::add method) + How to merge one result set into another (for example, using the ArrayList: :addAll method)
The last item is not required for serial operations; the code is designed to support both serial and parallel operations.
We provide these operations to the collect method and let it collect the filtered stream.
Copy the code code as follows:
List<Person> olderThan20 =
people.stream()
.filter(person -> person.getAge() > 20)
.collect(ArrayList::new, ArrayList::add, ArrayList::addAll);
System.out.println("People older than 20: " + olderThan20);
The result of this code is the same as before, but there are many advantages to writing it this way.
First of all, our programming method is more focused and expressive, clearly conveying the purpose of collecting the results into an ArrayList. The first parameter of collect() is a factory or producer, and the subsequent parameter is an operation used to collect elements.
Second, since we are not performing explicit modifications in the code, we can easily perform this iteration in parallel. We let the underlying library handle the modifications, and it will take care of the coordination and thread safety issues, even though the ArrayList itself is not thread safe - nice job.
If conditions permit, the collect() method can add elements to different sublists in parallel, and then merge them into a large list in a thread-safe way (the last parameter is used for the merge operation) .
We've seen that there are so many benefits to using the collect() method over manually adding elements to a list. Let's look at an overloaded version of this method - it's simpler and more convenient - it takes a Collector as a parameter. This Collector is an interface that includes producers, adders, and combiners. In previous versions, these operations were passed into methods as independent parameters. Using Collector is simpler and can be reused. . The Collectors tool class provides a toList method that can generate a Collector implementation to add elements to an ArrayList. Let's modify the previous code and use the collect() method.
Copy the code code as follows:
List<Person> olderThan20 =
people.stream()
.filter(person -> person.getAge() > 20)
.collect(Collectors.toList());
System.out.println("People older than 20: " + olderThan20);
A concise version of the collect() method of the Collectors tool class is used, but it can be used in more than one way. There are several different methods in the Collectors tool class to perform different collection and addition operations. For example, in addition to the toList() method, there is also the toSet() method, which can be added to a Set, the toMap() method, which can be used to collect into a key-value set, and the joining() method, which can be spliced into a string. We can also combine methods such as mapping(), collectingAndThen(), minBy(), maxBy() and groupingBy() for use.
Let's use the groupingBy() method to group people by age.
Copy the code code as follows:
Map<Integer, List<Person>> peopleByAge =
people.stream()
.collect(Collectors.groupingBy(Person::getAge));
System.out.println("Grouped by age: " + peopleByAge);
Simply call the collect() method to complete the grouping. groupingBy() accepts a lambda expression or method reference - this is called a classification function - and it returns the value of a certain attribute of the object that needs to be grouped. According to the value returned by our function, the elements in the calling context are placed into a certain group. The results of the grouping can be seen in the output:
Copy the code code as follows:
Grouped by age: {35=[Greg - 35], 20=[John - 20], 21=[Sara - 21, Jane - 21]}
The people have been grouped by age.
In the previous example we grouped people by age. A variant of the groupingBy() method can group by multiple conditions. The simple groupingBy() method uses a classifier to collect elements. The general groupingBy() collector can specify a collector for each group. In other words, elements will pass through different classifiers and collections during the collection process, as we will see below.
Continuing with the example above, this time instead of grouping by age, we just get the names of the people and sort them by their age.
Copy the code code as follows:
Map<Integer, List<String>> nameOfPeopleByAge =
people.stream()
.collect(
groupingBy(Person::getAge, mapping(Person::getName, toList())));
System.out.println("People grouped by age: " + nameOfPeopleByAge);
This version of groupingBy() accepts two parameters: the first is age, which is the condition for grouping, and the second is a collector, which is the result returned by the mapping() function. These methods all come from the Collectors tool class and are statically imported in this code. The mapping() method accepts two parameters, one is the attribute used for mapping, and the other is the place where the objects are to be collected, such as list or set. Let’s take a look at the output of the above code:
Copy the code code as follows:
People grouped by age: {35=[Greg], 20=[John], 21=[Sara, Jane]}
As you can see, people's names have been grouped by age.
Let's look at a combination operation again: group by the first letter of the name, and then select the oldest person in each group.
Copy the code code as follows:
Comparator<Person> byAge = Comparator.comparing(Person::getAge);
Map<Character, Optional<Person>> oldestPersonOfEachLetter =
people.stream()
.collect(groupingBy(person -> person.getName().charAt(0),
reducing(BinaryOperator.maxBy(byAge))));
System.out.println("Oldest person of each letter:");
System.out.println(oldestPersonOfEachLetter);
We first sorted the names alphabetically. To achieve this, we pass a lambda expression as the first parameter of groupingBy(). This lambda expression is used to return the first letter of the name for grouping. The second parameter is no longer mapping(), but performs a reduce operation. Within each group, it uses the maxBy() method to derive the oldest element from all elements. The syntax looks a bit bloated due to the many operations it combines, but the whole thing reads like this: Group by first letter of the name, and then work down to the eldest in the group. Consider the output of this code, which lists the oldest person in a group of names starting with a given letter.
Copy the code code as follows:
Oldest person of each letter:
{S=Optional[Sara - 21], G=Optional[Greg - 35], J=Optional[Jane - 21]}
We have already experienced the power of the collect() method and the Collectors utility class. In the official documentation of your IDE or JDK, take some time to study the Collectors tool class and become familiar with the various methods it provides. Next we will use lambda expressions to implement some filters.