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.
Collectors.teeing()
The Collectors.teeing() was introduced in Java 12. It collects stream elements twice concurrently, produces two results, and then uses the specified function to merge them into one final object:
— Collector<T,?,R> teeing(Collector<T,?,R1> downstream1, Collector<T,?,R2> downstream2, BiFunction<R1,R2,R> merger) – returns a Collector that is a composite of two downstream collectors; every element passed to the resulting collector is processed by both downstream collectors, then their results are merged using the specified merge function into the final result.
To demonstrate the collector’s behavior, we are going to use Person class:
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 String getName() { return name; }
public int getAge() { return age; }
@Override
public String toString() {
return "Person{name='" + name +
"', city='" + city + "', age=" + age + '}';
}
}
And we are going to use a stream created of the following List<Person>:
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 code uses two collectors to process the stream. One filters the stream element and selects only those persons that live in Denver, then it counts how many of such persons. The second collector just counts how many persons are emitted by the stream. The two results are collected in a List:
List<Long> result = list.stream()
.collect(Collectors.teeing(
Collectors.filtering(p -> p.getCity().equals("Denver"),
Collectors.counting()),
Collectors.counting(),
(r1, r2) -> List.of(r1, r2))
);
System.out.print(result); //prints: [3, 6]
As you can see, the above example requires the client code to know the meaning of the numbers in the resulting List – the first being the number of persons living in Denver, the second – the total count of processed persons.
We can improve this code (and remove the assumption on the order of the results in the resulting List) by placing the results in a Map with a label (key) for each result:
Map<String, Long> result = list.stream()
.collect(Collectors.teeing(
Collectors.filtering(p -> p.getCity().equals("Denver"),
Collectors.counting()),
Collectors.counting(),
(r1, r2) -> {
Map<String, Long> map = new HashMap();
map.put("Live in Denver", r1);
map.put("Total persons", r2);
return map;
}
));
System.out.print(result);
//prints: {Live in Denver=3, Total persons=6}
As you can see, now the results are clearly labeled, which does not require any assumptions on the client part.
The following is another example of the Collector.teeing() usage. This time we collected two lists: the list of persons living in Denver and the list of persons 23 years of age:
List<List<Person>> result = list.stream()
.collect(Collectors.teeing(
Collectors.filtering(p -> p.getCity().equals("Denver"),
Collectors.toList()),
Collectors.filtering(p -> 23 == p.getAge(),
Collectors.toList()),
(l1, l2) -> List.of(l1, l2)
));
System.out.println(result);
//prints: [[Person{name='John', city='Denver', age=25},
// Person{name='Jane', city='Denver', age=24},
// Person{name='Bob', city='Denver', age=23}],
// [Person{name='Bob', city='Denver', age=23},
// Person{name='Jill', city='Boston', age=23}]]
The following is another version of the same result, but this time we have collected only names of the persons and labeled each list in a Map:
Map<String, String> result4 = list.stream()
.collect(Collectors.teeing(
Collectors.filtering(p -> p.getCity().equals("Denver"),
Collectors.toList()),
Collectors.filtering(p -> 23 == p.getAge(),
Collectors.toList()),
(l1, l2) -> {
String s1 = l1.stream().map(p -> p.getName())
.collect(Collectors.joining(","));
String s2 = l2.stream().map(p -> p.getName())
.collect(Collectors.joining(","));
Map<String, String> map = new HashMap();
map.put("Live in Denver", s1);
map.put("Persons age 23", s2);
return map;
}
));
System.out.println(result4);
//prints: {Persons age 23=Bob,Jill,
// Live in Denver=John,Jane,Bob}
This completes our overview of Java 8 Stream API as of Java 13. We plan to add more articles if new Stream operations or collectors will be added to the JDK. We also will share some interesting and challenging examples of the Stream API usage.
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.