Collectors collectingAndThen

Introduction

I have written about the various Collectors we can get from the Collectors class and use it in the collect step of the Java stream. In this post, we will learn about the Collectors collectingAndThen collector.

Collectors collectingAndThen method

The collectingAndThen is a static utility method in the Collectors class which returns a new Collector. The returned Collector adapts a passed Collector to perform a finishing transformation (a Function).

Collectors collectingAndThen collector
Collectors collectingAndThen collector

We have a Collector which operates on input elements of type T and its result type is R. Using collectingAndThen we can build a Collector which enables us to attach a transformation function to the first collector to map from type R to type RR. The result is that we have a new collector whose input type is T and result type is RR. The finishing function is a Function<R, RR> which is applied to the final result of the downstream collector. 

Thus we can say that collectingAndThen returns a collector which performs the action of the downstream collector, followed by an additional finishing transformation step.

Collectors collectingAndThen with toList and toSet

Let us see how we can use Collectors collectingAndThen with Collectors#toList and Collectors#toSet.

With Collectors#toList

Let us say we have a list of integers which we want to convert as a list of strings.

List<Integer> ints = List.of(1, 2, 3, 4, 3);
List<String> result = ints.stream()
        .map(String::valueOf)
        .collect(Collectors.toList());

As per the Collectors.toList contract, the type of the list it returns is not documented. Say we want the result to be an unmodifiable list. So, we have to wrap the above list as,

Collections.unmodifiableList(result);

Or, wrap the entire stream pipeline as,

List<String> result = Collections.unmodifiableList(
        ints.stream()
                .map(String::valueOf)
                .collect(Collectors.toList())
);

To avoid this, we can use Collectors.collectingAndThen by passing a finisher function to create an unmodifiable list from the result list the Collectors#toList returns.

List<String> result = ints.stream()
        .map(String::valueOf)
        .collect(Collectors.collectingAndThen(Collectors.toList(),
                Collections::unmodifiableList));
System.out.println(result); //[1, 2, 3, 4, 3]

As an another example, let us say we want the result list to be a LinkedList. We can create a LinkedList from the result list of the Collectors.toList as:

result = ints.stream()
        .map(String::valueOf)
        .collect(Collectors.collectingAndThen(Collectors.toList(),
                LinkedList::new));
System.out.println(result); //[1, 2, 3, 4, 3]
System.out.println(result.getClass()); // class java.util.LinkedList

With Collectors#toSet

Similar to how we used collectingAndThen with toList, we can do the same with Collectors.toSet as well. To create an unmodifiable set, call Collections.unmodifiableSet from the finisher function.

Set<String> resultSet = ints.stream()
        .map(String::valueOf)
        .collect(Collectors.collectingAndThen(Collectors.toSet(),
                Collections::unmodifiableSet));
System.out.println(resultSet); //[1, 2, 3, 4]

Note: Since it is a set, it doesn’t contain duplicate elements.

Collectors collectingAndThen with Collectors toMap

Let us use Collectors#collectingAndThen with Collectors.toMap.

As before, let us start with a stream of ints and create a map Map<String, Integer> as shown below:

List<Integer> ints = List.of(1, 2, 3, 4);
Map<String, Integer> map = ints.stream()
        .collect(Collectors.toMap(String::valueOf, Function.identity()));

To make the map immutable, we can call Collections#unmodifiableMap using collectingAndThen.

Map<String, Integer> map = ints.stream()
        .collect(Collectors.collectingAndThen(
                Collectors.toMap(String::valueOf, Function.identity()),
                Collections::unmodifiableMap));
System.out.println(map); // {1=1, 2=2, 3=3, 4=4}

Or, we can make the resultant map a TreeMap as,

map = ints.stream()
        .collect(Collectors.collectingAndThen(
                Collectors.toMap(String::valueOf, Function.identity()),
                TreeMap::new));
System.out.println(map); //{1=1, 2=2, 3=3, 4=4}

Collectors collectingAndThen with Collectors groupingBy

Let us create a simple class called Student as shown below. Each student has a name, age, and department.

public class Student {
    private String name;
    private int age;
    private String department;

    public Student(String name, int age, String department) {
        this.name = name;
        this.age = age;
        this.department = department;
    }

    public String getName() {
        return name;
    }

    public int getAge() {
        return age;
    }

    public String getDepartment() {
        return department;
    }

    @Override
    public String toString() {
        return name;
    }
}

Let us create a list of students and use Collectors#groupingBy to group the students by each department.

List<Student> students = List.of(
        new Student("John", 19, "CS"),
        new Student("Adam", 21, "Math"),
        new Student("Steve", 20, "CS"),
        new Student("Perry", 22, "Science"),
        new Student("Kyle", 20, "Math")
);
Map<String, List<Student>> studentsByDept = students.stream()
        .collect(Collectors.groupingBy(Student::getDepartment));
System.out.println(studentsByDept); //{CS=[John, Steve], Science=[Perry], Math=[Adam, Kyle]}

We can use collectingAndThen to map the resultant map from toMap collector to a TreeMap.

studentsByDept = students.stream()
        .collect(Collectors.collectingAndThen(
                Collectors.groupingBy(Student::getDepartment),
                TreeMap::new));
System.out.println(studentsByDept); //{CS=[John, Steve], Math=[Adam, Kyle], Science=[Perry]}

Since a TreeMap orders the map entries as per the key’s natural order, we can see from the output that the entries are sorted lexicographically (CS, Math and Science).

Note that the above can be achieved by using a overloaded Collectors.groupingBy method which accepts a mapFactory which is a supplier for creating an empty map into which the results will be inserted.

studentsByDept = students.stream()
        .collect(Collectors.groupingBy(Student::getDepartment, TreeMap::new,
                Collectors.toList()));
System.out.println(studentsByDept);

Collectors collectingAndThen with Collectors mapping

Here, in this example, I have used Collectors.mapping directly as the first-level collector. But generally we use Collectors.mapping as a downstream collector.

Below, we map the student name from each Student instance and collect it as a list.

List<String> studentNames = students.stream()
        .collect(Collectors.mapping(Student::getName, Collectors.toList()));
System.out.println(studentNames); //[John, Adam, Steve, Perry, Kyle]

We can wrap the call to Collectors.mapping with Collectors.collectingAndThen and wrap the list as an unmodifiable list.

studentNames = students.stream()
        .collect(Collectors.collectingAndThen(
                        Collectors.mapping(Student::getName, Collectors.toList()),
                        Collections::unmodifiableList));
System.out.println(studentNames); //[John, Adam, Steve, Perry, Kyle]

Chaining with collectingAndThen

So far we have applied Collectors collectingAndThen to various Collectors (toMaptoListtoSetgroupingBy and mapping). An important thing to note here is that calling Collectors.collectingAndThen returns a Collector. Hence, this collector can be used where a normal Collector is needed. In other words, we can use the Collector returned by collectingAndThen as a downstream collector as well.

For example, let us use groupingBy on the stream of Students and map each student to the name by using Collectors.mapping and collect as a list (using toList).

Map<String, List<String>> studentNamesByDept = students.stream()
        .collect(Collectors.groupingBy(Student::getDepartment,
                Collectors.mapping(Student::getName, Collectors.toList())));
System.out.println(studentNamesByDept); //{CS=[John, Steve], Science=[Perry], Math=[Adam, Kyle]}

Now, let us use collectingAndThen on the Collectors.mapping to convert the list returned to an unmodifiableList. This collector is then passed as the downstream collector of groupingBy.

//groupingBy(func1, collectingAndThen(collector, func2))

studentNamesByDept = students.stream()
        .collect(Collectors.groupingBy(Student::getDepartment,
                Collectors.collectingAndThen(
                        Collectors.mapping(Student::getName, Collectors.toList()),
                        Collections::unmodifiableList)));
System.out.println(studentNamesByDept); //{CS=[John, Steve], Science=[Perry], Math=[Adam, Kyle]}

Conclusion

In this post, we learnt about the Collectors collectingAndThen collector. We applied it to various other collectors. We also saw how we can chain the Collector returned by collectingAndThen (used as a downstream collector).

Useful References

https://stackoverflow.com/questions/47810524/is-collectingandthen-method-enough-efficient

Leave a Reply