Java Collectors Introduction

Collectors where introduced in Java 8. They are commonly used as the final step of Stream Processing in Java. Collectors provide different operations such as collecting elements into a collection, performing reductions such as summarizing, averaging, grouping etc.

Collecting Elements

Java 8 Collectors are commonly used as a terminal operation during Java Stream Processing. The Stream.collect() method is commonly called to covert a stream into a desired data structure such as a Java collection. The collect method works with the type java.util.stream.Collector.

1
public interface Collector<T, A, R> { }

Collectors

To simplify working with Collector instances the class java.util.stream.Collectors provides a number of useful utility methods. In the following examples we will examine some of these methods.

In the following examples we will use the following list of integers:

1
List<Integer> listOfIntegers = Arrays.asList(1,2,3,4,5,6,7,8,9,10);

Collectors.toList()

The toList() method can be used for collecting all stream elements into a java.util.List.

1
2
3
4
5
6
@Test
public void toList() {
    List<Integer> list = listOfIntegers.stream()
            .collect(Collectors.toList());
    assertThat(list, is(listOfIntegers));
}

Above we create a stream from the listOfIntegers and collect them into a new List. Moreover, we can’t control the type of java.util.List returned. If you require more control over the type of List returned you can use the toCollection method instead.

Collectors.toSet()

The toSet() method can be used to collect the elements of a stream into a set.

1
2
3
4
5
6
7
8
9
@Test
public void toSet() {
    Set<Integer> expected = new HashSet<>(listOfIntegers);

    Set<Integer> set = listOfIntegers.stream()
            .collect(Collectors.toSet());

    assertThat(expected, is(set));
}

Above we, convert a stream into a Set instance with all duplicates removed.

Collectors.toMap()

The Collectors toMap() method can be used to convert a Stream into a Map instance. The toMap() method is a little bit more complicated than the previous methods we looked at.

To convert a stream to a Map instance we have to pass two Function<T,R> instances.

1
2
Collector<T, ?, Map<K,U>> toMap(Function<? super T, ? extends K> keyMapper,
                                Function<? super T, ? extends U> valueMapper)
  • keyMapper: used to resolve the key that will represent a stream element in the Map.
  • valueMapper: used to resolve the object from the stream element that will represent a Map value for the key resolved by the keyMapper.
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
@Test
public void toMap() {
    Map<Integer, Integer> expected = new HashMap<>();

    for (Integer i : listOfIntegers) {
        expected.put(i, i);
    }

    Function<Integer, Integer> keyMapper = (i) -> i;
    Function<Integer, Integer> valueMapper = keyMapper;

    Map<Integer, Integer> map = listOfIntegers.stream()
            .collect(Collectors.toMap(keyMapper, valueMapper));

    assertThat(expected, is(map));
}

Above we convert the listOfIntegers to a Map<Integer,Integer> using the functions:

1
2
Function<Integer, Integer> keyMapper = (i) -> i;
Function<Integer, Integer> valueMapper = keyMapper;

These functions simply return the value of the stream element as the key and the corresponding value of the Map.

We can simplify the above code by using the Function.identity() that returns the same value as passed as its argument.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
@Test
public void toMap_withIdentifyFunction() {
    Map<Integer, Integer> expected = new HashMap<>();

    for (Integer i : listOfIntegers) {
        expected.put(i, i);
    }

    Map<Integer, Integer> map = listOfIntegers.stream()
            .collect(Collectors.toMap(Function.identity(), Function.identity()));

    assertThat(expected, is(map));
}

However, the implementation of the toMap method that we have being using will throw an IllegalStateException if duplicates are added to the Map. To deal with such cases we have to use an alternative version of the toMap method.

1
2
3
Collector<T, ?, Map<K,U>> toMap(Function<? super T, ? extends K> keyMapper,
                                Function<? super T, ? extends U> valueMapper,
                                BinaryOperator<U> mergeFunction)

Using the above method we can pass a mergeFunction to define how duplicates should be handled.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
@Test
public void toMap_withMergeFunction() {
    List<Integer> listToConvert = Arrays.asList(1,1,2);
    Map<Integer, Integer> expected = new HashMap<>();

    for (Integer i : listToConvert) {
        expected.put(i, i);
    }

    BinaryOperator<Integer> mergeFunction = (k,v) -> k;

    Map<Integer, Integer> map = listToConvert.stream()
            .collect(Collectors.toMap(Function.identity(), Function.identity(), mergeFunction));

    assertThat(expected, is(map));
}

Collectors.toCollection()

We can use the toCollection() method to gain more control over the type of collection returned.

1
2
3
4
5
6
@Test
public void toList_toCollection() {
    List<Integer> list = listOfIntegers.stream()
            .collect(Collectors.toCollection(LinkedList::new));
    assertThat(list, is(listOfIntegers));
}

Collectors.collectingAndThen()

The collectingAndThen method allows for performing another action on the collected collection of elements.

1
2
3
4
5
6
7
 @Test
public void collectingAndThen() {
    List<Integer> expectedList = Arrays.asList(1);
    List<Integer> list = listOfIntegers.stream()
            .collect(Collectors.collectingAndThen(Collectors.toList(), (c) -> c.subList(0, 1)));
    assertThat(expectedList, is(list));
}

Above, after we convert the stream to a List we return a sub list of the created List instance.

Collectors.counting()

The counting method simply returns the count of the elements in a Stream.

1
2
3
4
5
6
7
8
@Test
public void counting() {
    Long sizeOfList = listOfIntegers.stream()
            .collect(Collectors.counting());

    Integer expectedSize = listOfIntegers.size();
    assertEquals(expectedSize, expectedSize);
}

Collectors.mapping()

The mapping method can be used to execute a transformation before collecting the elements of a Stream.

1
2
3
4
5
6
7
8
@Test
public void mapping() {
    List<Integer> listOfIntegersSquared = listOfIntegers.stream()
            .collect(Collectors.mapping((i) -> i * i, Collectors.toList()));

    List<Integer> expectedList = Arrays.asList(1,4,9,16,25,36,49,64,81,100);
    assertThat(expectedList, is(listOfIntegersSquared));
}

Above we square all the integers before collecting the result into a List.

Collectors.joining()

The Collectors.joining method can be used to join a Stream of String elements.

1
2
3
4
5
6
7
8
@Test
public void joining() {
    List<String> listOfString = Arrays.asList("1","2","3");

    String joinedStr = listOfString.stream()
            .collect(Collectors.joining("-"));
    assertEquals("1-2-3", joinedStr);
}

Collectors.groupingBy()

The Collectors.groupingBy method is used to group elements of a stream by some property and add the result to a Map instance.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
@Test
public void groupingBy() {
    Map<Integer,List<Integer>> expectedMap = new HashMap<>();
    expectedMap.put(1, Arrays.asList(1,2,3,4,5,6,7,8,9));
    expectedMap.put(2, Arrays.asList(10));

    Map<Integer,List<Integer>> map = listOfIntegers.stream()
            .collect(Collectors.groupingBy((i) -> i.toString().length(), Collectors.toList()));

    assertThat(expectedMap, is(map));
}

Above we group the integers by the number of digits.

Collectors.partitioningBy()

The Collectors.partitioningBy method is a special form of groupingBy that uses a predicate instance and returns a Map instance with a Boolean value as the key and a Collection instance as the value.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
@Test
public void partitioningBy() {
    Map<Boolean, List<Integer>> expectedMap = new HashMap<>();
    expectedMap.put(false, Arrays.asList(1, 3, 5, 7, 9));
    expectedMap.put(true, Arrays.asList(2, 4, 6, 8, 10));

    Map<Boolean, List<Integer>> map = listOfIntegers.stream()
            .collect(Collectors.partitioningBy((i) -> i % 2 == 0, Collectors.toList()));

    assertThat(expectedMap, is(map));
}

Above we group the listOfIntegers into collections of even and odd numbers.

Averaging

Collectors provides a number of methods for finding an average.

1
2
3
4
5
6
7
8
9
@Test
public void findAverage() {
    Double average = listOfIntegers.stream()
            .collect(Collectors.averagingInt((i) -> i));

    Double expectedAverage = 5.5;

    assertEquals(expectedAverage, average);
}

Summing

The Collectors utility class also offers various methods for summing elements of a Stream.

1
2
3
4
5
6
7
@Test
public void summing() {
    Integer expectedSum = 55;
    Integer sum = listOfIntegers.stream()
            .collect(Collectors.summingInt((i) -> i));
    assertEquals(expectedSum, sum);
}

Above we sum the elements of the listOfIntegers.

Conclusion

In this tutorial we introduced Collectors in Java 8. We showed that Collectors are normally used by the Stream.collect() method. We also looked at some of the common collector operations.