Java 8 Collectors is used at the final step of processing a Stream.
You will gain a deep understanding of all the methods of the Collectors class, complete with examples and explanations to solidify your knowledge.
In this guide, we will explore the various methods of the Collector class and learn how to leverage them to streamline and optimize Java stream operations.

Java 8 Collectors

Collectors class is used to combine the results of a java stream into a summary.
It has methods that are often used in combination with the Stream.collect() method to perform specific operations on the elements of the stream and store the results in a mutable container.

One of the most common use cases for Collectors is to convert the elements of a Stream into a List, Set, or Map.
This can be achieved using the toList(), toSet(), and toMap() collectors, respectively.

toList()

toList() is a static method of Collectors class and it used to accumulate stream elements to a list. Example,

List list = stream.
            collect(Collectors.toList());

toSet()

Collectors also provides the capability to accumulate the elements of a stream into a new Set using the toSet() collector.
This eliminates any duplicate elements present in the stream.

Set set = stream.
          collect(Collectors.toSet());

Collectors.toSet() eliminates duplicate elements and accumulates the elements of the stream into a new Set.

toMap()

Data can be transformed into a Map using the toMap() method.
toMap() takes two functions as arguments to extract the key and value from the stream elements and accumulates them into a new Map.

These two functions are of type java.util.function.Function interface and are referred as Key Mapper and Value Mapper respectively.

Suppose there is a stream of employees and you want to convert it to a map with their id as key and name as value.

Below is an example of toMap() for this purpose.

Map<String, Integer> map = employees.
                           stream().
                           collect(
                            Collectors.toMap(Student::getId, Student::getName
                           ));

If the keys are duplicate, then toMap() will throw an IllegalStateException, since it does not know which entry to add to the map.

Grouping and Partitioning Data

Data in a stream can be grouped or partitioned using the groupingBy() and partitioningBy() collectors.
These collectors provide powerful features to group and partition data based on specific criteria.

Both these methods return a Map.

Below is an example of grouping a stream of string elements based on their length.

Map<Integer, List<String>> lengthGroups = Stream.of("apple", "banana", "orange", "grape")
                .collect(Collectors.groupingBy(String::length));

Output map is

{5=[apple, grape], 6=[banana, orange]}

Partitioning is a specialized form of grouping, where elements are divided into two groups based on a predicate.
Suppose you have a list of integers and you want to partition them based on even and odd numbers.
Below is how to do it with partitioningBy().

List<Integer> numbers = List.of(1, 2, 3, 4, 5, 6);
Map<Boolean, List<Integer>> lengthGroups = numbers.stream()
				.collect(Collectors.partitioningBy(num -> num % 2 == 0));

Output map will have below contents

{false=[1, 3, 5], true=[2, 4, 6]}

Statistical Collectors: counting(), summingInt(), averagingInt()

With counting(), summingInt(), and averagingInt() collectors, you can perform statistical operations on the elements of a Stream.
These collectors can be used to
A. count the elements in the stream,
B. get the sum of a numerical attribute, or
C. obtain the average of a numerical attribute, respectively.

long count = stream.collect(Collectors.counting());
int sum = stream.collect(Collectors.summingInt(String::length));
double average = stream.collect(Collectors.averagingInt(String::length));

With these collectors, you can easily perform statistical operations on the elements of a Stream to gain valuable insights into the data.

Joining Strings

String concatenation becomes seamless with the joining() collector.
It eliminates the need for manual string concatenation in stream operations.

String concatenatedString = Stream.of("Java", "8", "Collectors").
                            collect(Collectors.joining(", "));

This will join elements of stream separated with a comma and return a single string.

Finding max element

The maxBy() collector is employed to find the maximum element in a stream based on a comparator.
Let’s look at an example where we determine the oldest person in a list.

Optional<Person> oldestPerson = people.
                                stream().
                                collect(
                                  Collectors.maxBy(Comparator.comparingInt(Person::getAge))
                                );

maxBy() accepts a Comparator to determine the ordering of elements.

Finding min element

minBy() collector identifies the minimum element in a stream based on a specified comparator. Here’s an example showcasing how to find the person with the lowest age:

javaCopy code

Optional<Person> youngestPerson = people.
                                  stream().
                                  collect(
                                   Collectors.minBy(Comparator.comparingInt(Person::getAge))
                                  );

In this example, the minByy() collector, paired with a comparator, locates the person with the lowest age in the given list.

Understanding Parallelism with Collectors

The use of parallel streams and Collectors introduces the concept of parallelism.
When using parallel streams with Collectors, the reduction operation is performed concurrently on multiple threads, potentially improving performance for large datasets.
However, it’s important to understand the implications of parallelism, including potential race conditions, thread synchronization, and the overall impact on performance.

List numbers = Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9, 10);
int sum = numbers.parallelStream()
                 .collect(Collectors.summingInt(Integer::intValue));

It’s essential to be mindful of the trade-offs and considerations associated with parallelism when using Collectors in Java 8.
Performance improvements from parallelism must be balanced against potential overhead, thread safety concerns, and the added complexity of debugging and maintaining parallel collector operations.

Conclusion

In conclusion, the Collector class in Java provides a wide range of methods to perform various operations on stream data.
The toList(), toMap(), and toCollection() methods allow for easy conversion of streams to different data structures.
The joining() method concatenates stream elements into a single string, while counting() gives the total count of elements in the stream.
The averagingDouble(), summingDouble(), maxBy(), and minBy() methods provide ways to calculate average, sum, maximum, and minimum values from the stream.
The groupingBy() method allows for grouping elements based on a classifier function.
Additionally, the collectingAndThen() and teeingBy() methods offer more advanced functionalities for stream operations.
By leveraging the Collector class methods, developers can efficiently process and manipulate stream data in Java applications.