Terminal operation either returns one value (of the same or another type than the input type) or does not return anything at all (produces only side effects). It does not allow another operation to be applied afterward and closes the stream.
In this post, we will continue covering the last of the terminal operations called collect():
R collect(Collector<T,A,R> collector)
It is a specialization of the reduce() operation. It allows implementing a vast variety of algorithms using the ready-to-use collectors from the java.util.stream.Collectors class. We discussed how to create a custom collector in Java streams 25. Collect 1. Custom collector. In this article, we will use only the collectors produced by the Collectors class.
Creating Map object using Collectors.reducing() collector
The Collectors.reducing() collector processes the stream values and collects (reduces) the results into one structure.
There are two overloaded versions of factory methods that create the Collectors.reducing() collector:
— Collector<T,?,Optional<T>> reducing(BinaryOperator<T> op) – returns a Collector which performs a reduction of its input elements under a specified BinaryOperator;
— Collector<T,?,T> reducing(T identity, BinaryOperator<T> op) – returns a Collector which performs a reduction of its input elements under a specified BinaryOperator using the provided identity;
— Collector<T,?,U> reducing(U identity, Function<T,U> mapper, BinaryOperator<U> op) – returns a Collector which performs a reduction of its input elements under a specified mapping function and BinaryOperator.
The BinaryOperator<T> is shorthand of Function<T,T,T> that receives two elements of type T and produces a result of type T, too.
The Collectors.reducing() collector can be used by passing into the collect() but is more useful as a parameter for the Collectors.groupingBy() and Collectors.partitioningBy().
In our code examples, we will be using the following Person class as a type of stream elements:
class Person {
private String name, city;
private int age;
public Person(String name, String city, int age) {
this.name = name;
this.city = city;
this.age = age;
}
public String getCity() { return city; }
public int getAge() { return age; }
@Override
public String toString() {
return "Person{name='" + name + '\'' +
", city='" + city + "\', age=" + age + "}";
}
}
And here is the list of Person objects we are going to stream in each of our examples:
List<Person> list =
List.of(new Person("John", "Denver", 25),
new Person("Jane", "Denver", 24),
new Person("Bob", "Denver", 23),
new Person("Bill", "Boston", 25),
new Person("Bob", "Chicago", 24),
new Person("Jill", "Boston", 23));
The following is the code example of how the first of the overloaded versions of the Collectors.reducing() collector can be used:
Optional<Person> oldestOptional = list.stream()
.collect(Collectors
.reducing(BinaryOperator
.maxBy(Comparator.
comparing(Person::getAge))));
System.out.print(oldestOptional.get());
//prints: Person{name='John', city='Denver', age=25}
But the same result can be achieved much simpler without collect() and Collector.reducing():
Optional<Person> oldestOptional = list.stream()
.reduce(BinaryOperator.maxBy(Comparator
.comparing(Person::getAge)));
System.out.println(oldestOptional.get());
//prints: Person{name='John', city='Denver', age=25}
As you can see, there is no advantage of using the Collectors.reducing() collector for such a case.
That is because the Collectors.reducing() was created to be used with groupingBy() and partitioningBy() for multilayered grouping.
Before demonstrating how to do it, let us first refresh how groupingBy() and partitioningBy() work without the Collectors.reducing() collector:
Map<String, List<Person>> map = list.stream()
.collect(Collectors.groupingBy(Person::getCity));
System.out.print(map);
//Prints:
// {Chicago=[Person{name='Bob', city='Chicago', age=24}],
// Denver=[Person{name='John', city='Denver', age=25},
// Person{name='Jane', city='Denver', age=24},
// Person{name='Bob', city='Denver', age=23}],
// Boston=[Person{name='Bill', city='Boston', age=25},
// Person{name='Jill', city='Boston', age=23}]}
Map<Boolean, List<Person>> map = list.stream()
.collect(Collectors
.partitioningBy(p -> "Denver".equals(p.getCity())));
System.out.println(map);
//prints:
// {false=[Person{name='Bill', city='Boston', age=25},
// Person{name='Bob', city='Chicago', age=24},
// Person{name='Jill', city='Boston', age=23}],
// true=[Person{name='John', city='Denver', age=25},
// Person{name='Jane', city='Denver', age=24},
// Person{name='Bob', city='Denver', age=23}]}
As you can see, the groupingBy() groups objects by the value specified as the parameter (Person.getCity(), in our example). The partitioningBy() groups objects in two categories: those that match the specified predicate (Person.getCity() has to return “Denver”, in our example) and those that do not match the predicate.
Now let us add the Collectors.reducing() collector – the one we have demonstrated already – which calculates the oldest person:
Map<String, Optional<Person>> map = list.stream()
.collect(Collectors.groupingBy(Person::getCity,
Collectors.reducing(BinaryOperator
.maxBy(Comparator.comparing(Person::getAge)))));
System.out.print(map);
//prints:
// {Chicago=Optional[Person{name='Bob', city='Chicago', age=24}],
// Denver=Optional[Person{name='John', city='Denver', age=25}],
// Boston=Optional[Person{name='Bill', city='Boston', age=25}]}
Map<Boolean, Optional<Person>> map = list.stream()
.collect(Collectors
.partitioningBy(p -> "Denver".equals(p.getCity()),
Collectors.reducing(BinaryOperator
.maxBy(Comparator
.comparing(Person::getAge)))));
System.out.println(map5);
//prints:
// {false=Optional[Person{name='Jane', city='Denver', age=24}],
// true=Optional[Person{name='John', city='Denver', age=25}]}
As you can see, the provided Collectors.reducing() collector calculates the oldest person in each group and wraps the result in an Optional. If the stream were empty the result would be Optional.empty().
To remove the wrapping Optional, we can use the second version of the Collector.reducing(). It takes an Identity as the first element which is going to be used together with the first stream element as the input to the BinaryOperator<T>.
To remind you, the BinaryOperator<T> is a shorthand version of the Function<T,T,T>, which means that the Identity type has to be the same as the stream elements type and the same as the result.
Again, the Identity value will be passed into the BinaryOperator<T> along with the first element emitted by the stream. In our case, the BinaryOperator<T> is going to compare the two input values and select the bigger one as the result. Since the presence of at least one element (passed-in as an Identity) is guaranteed, the result is also always present (even if the stream is empty) – that’s why there is no Optional wrapper.
The question is which Person value to pass as an Identity? Since we are looking for the biggest age, it has to be an object with age value smaller than any age in Person objects emitted by the stream:
Map<String, Optional<Person>> map = list.stream()
.collect(Collectors.groupingBy(Person::getCity,
Collectors.reducing(new Person("x", "x", 0),
BinaryOperator.maxBy(Comparator
.comparing(Person::getAge)))));
System.out.print(map);
//prints:
// {Chicago=Person{name='Bob', city='Chicago', age=24},
// Denver=Person{name='John', city='Denver', age=25},
// Boston=Person{name='Bill', city='Boston', age=25}}
Map<Boolean, Optional<Person>> map = list.stream()
.collect(Collectors
.partitioningBy(p -> "Denver".equals(p.getCity()),
Collectors.reducing(new Person("x", "x", 0),
BinaryOperator.maxBy(Comparator
.comparing(Person::getAge)))));
System.out.println(map5);
//prints:
// {false=Person{name='Jane', city='Denver', age=24},
// true=Person{name='John', city='Denver', age=25}}
If we set age in the initial value to 200, for example, then it would be identified as the age of the oldest person:
Map<String, Optional<Person>> map = list.stream()
.collect(Collectors.groupingBy(Person::getCity,
Collectors.reducing(new Person("x", "x", 200),
BinaryOperator.maxBy(Comparator
.comparing(Person::getAge)))));
System.out.print(map);
//prints:
// {Chicago=Person{name='x', city='x', age=200},
// Denver=Person{name='x', city='x', age=200},
// Boston=Person{name='x', city='x', age=200}}
Map<Boolean, Optional<Person>> map = list.stream()
.collect(Collectors
.partitioningBy(p -> "Denver".equals(p.getCity()),
Collectors.reducing(new Person("x", "x", 200),
BinaryOperator.maxBy(Comparator
.comparing(Person::getAge)))));
System.out.println(map5);
//prints:
// {false=Person{name='x', city='x', age=200},
// true=Person{name='x', city='x', age=200}}
As you can see, the initial value (Identity object) was used for the calculation of the oldest person in each group. That is why it was always returned as the one having the biggest age.
We can also calculate the biggest age itself using Controllers.reducing():
int theOldestAge = list.stream()
.collect(Collectors.reducing(0, Person::getAge,
BinaryOperator.maxBy(Comparator
.comparingInt(i -> i))));
System.out.print(theOldestAge); //prints: 25
But using map() and reduce() is a simpler way to do it:
int theOldestAge = list.stream()
.map(Person::getAge)
.reduce(0, BinaryOperator.maxBy(Comparator
.comparingInt(i -> i)));
System.out.print(theOldestAge); //prints: 25
So, as we mentioned at the beginning, using the Controller.reducing() makes sense only for multilayered grouping. Otherwise, using the reduce() operation provides a simpler solution.
The following is another example of a multilayered grouping – find the oldest person in each city – where the Controller.reducing() can do the job that the reduce() operation cannot or would be much more complicated:
Map<String, Integer> map = list.stream()
.collect(Collectors.groupingBy(Person::getCity,
Collectors.reducing(0, Person::getAge,
BinaryOperator.maxBy(Comparator
.comparingInt(i -> i)))));
System.out.print(map);
//prints: {Chicago=24, Denver=25, Boston=25}
Map<Boolean, Integer> map = list.stream()
.collect(Collectors
.groupingBy(p -> "Denver".equals(p.getCity()),
Collectors.reducing(0, Person::getAge,
BinaryOperator.maxBy(Comparator
.comparingInt(i -> i)))));
System.out.println(map); //prints: {false=24, true=25}
In the next post, we will talk about creating one value from a stream using Collectors.filtering() collector, typically used as a parameter for Collectors.groupingBy() or Collectors.partitioningBy() collector.
See other posts on Java 8 streams and posts on other topics.
You can also use navigation pages for Java stream related blogs:
— Java 8 streams blog titles
— Create stream
— Stream operations
— Stream operation collect()
The source code of all the code examples is here in GitHub.
Send your comments using the link Contact or in response to my newsletter.
If you do not receive the newsletter, subscribe via link Subscribe under Contact.