I just released the Starter Class of "Learn Spring Security":
1. Overview
In this tutorial we will be going through Java 8’s Collectors, which are used at the final step of processing a Stream.
If you want to read more about Stream API itself, check this article.
2. Collect Method
Stream.collect() is one of the Java 8’s Stream API‘s terminal methods. It allows to perform mutable fold operations (repackaging elements to some data structures and applying some additional logic, concatenating them, etc.) on data elements held in a Stream instance.
The strategy for this operation is provided via Collector interface implementation.
3. Collectors
All predefined implementations can be found in the Collectors class. It’s a common practice to use a following static import with them in order to leverage increased readability:
import static java.util.stream.Collectors.*;
or just single import collectors of your choice:
import static java.util.stream.Collectors.toList; import static java.util.stream.Collectors.toMap; import static java.util.stream.Collectors.toSet;
In the following examples we will be reusing the following list:
List<String> givenList = Arrays.asList("a", "bb", "ccc", "dd");
3.1. Collectors.toList()
ToList collector can be used for collecting all Stream elements into an List instance. The important thing to remember is the fact that we can’t assume any particular List implementation with this method. If you want to have more control over this, use toCollection instead.
Let’s create a Stream instance representing a sequence of elements and collect them into an List instance:
List<String> result = givenList.stream() .collect(toList());
3.2. Collectors.toSet()
ToSet collector can be used for collecting all Stream elements into an Set instance. The important thing to remember is the fact that we can’t assume any particular Set implementation with this method. If you want to have more control over this, use toCollection instead.
Let’s create a Stream instance representing a sequence of elements and collect them into an Set instance:
Set<String> result = givenList.stream() .collect(toSet());
3.3. Collectors.toCollection()
As you probably already noticed, when using toSet and toList collectors, you can’t make any assumptions of their implementations. If you want to use a custom implementation, you will need to use the toCollection collector with a provided collection of your choice.
Let’s create a Stream instance representing a sequence of elements and collect them into an LinkedList instance:
List<String> result = givenList.stream() .collect(toCollection(LinkedList::new))
Notice that this will not work with any immutable collections. In such case you would need to either write a custom Collector implementation or use collectingAndThen.
3.4. Collectors.toMap()
ToMap collector can be used to collect Stream elements into a Map instance. In order to do this, you need to provide two functions:
- keyMapper
- valueMapper
keyMapper will be used for extracting a Map key from a Stream element and valueMapper will be used for extracting a value associated with a given key.
Let’s collect those elements into a Map that stores strings as keys and their lengths as values:
Map<String, Integer> result = givenList.stream() .collect(toMap(Function.identity(), String::length))
Function.identity() is just a shortcut for defining function that accept and return the same value;
Sometimes you might encounter a situation where you might end up with a key collision. In such case you should use toMap with another signature.
Map<String, Integer> result = givenList.stream() .collect(toMap(Function.identity(), String::length, (i1, i2) -> i1));
The third argument here is a BinaryOperator, where you can specify how you want collisions to be handled. In this case we will just pick any of these two colliding values, because we know that same strings will always have same lengths too.
3.5. Collectors.collectingAndThen()
CollectingAndThen is a special collector that allows to perform another action on a Collector’s result straight after collecting ends.
Let’s collect Stream elements to a List instance and then convert the result into an ImmutableList instance:
List<String> result = givenList.stream() .collect(collectingAndThen(toList(), ImmutableList::copyOf))
3.6. Collectors.joining()
Joining collector can be used for joining Stream<String> elements.
We can join them together by doing:
String result = givenList.stream() .collect(joining());
which will result in:
"abbcccdd"
You can also specify custom separators, prefixes, postfixes:
String result = givenList.stream() .collect(joining(" "));
which will result in:
"a bb ccc dd"
or you can write:
String result = givenList.stream() .collect(joining(" ", "PRE-", "-POST"));
which will result in:
"PRE-a bb ccc dd-POST"
3.7. Collectors.counting()
Counting is a simple collector that allows simply counting of all Stream elements.
Now we can write:
Long result = givenList.stream() .collect(counting());
3.8. Collectors.summarizingDouble/Long/Int()
SummarizingDouble/Long/Int is a collector that returns a special class containing statistical information about numerical data in a Stream of extracted elements.
We can obtain information about string lengths by doing:
DoubleSummaryStatistics result = givenList.stream() .collect(summarizingDouble(String::length));
In this case following will be true:
assertThat(result.getAverage()).isEqualTo(2); assertThat(result.getCount()).isEqualTo(4); assertThat(result.getMax()).isEqualTo(3); assertThat(result.getMin()).isEqualTo(1); assertThat(result.getSum()).isEqualTo(8);
3.9. Collectors.averagingDouble/Long/Int()
AveragingDouble/Long/Int is a collector that simply returns an average of extracted elements.
We can get average string length by doing:
Double result = givenList.stream() .collect(averagingDouble(String::length));
3.10. Collectors.summingDouble/Long/Int()
SummingDouble/Long/Int is a collector that simply returns a sum ofextracted elements.
We can get a sum of all string lengths by doing:
Double result = givenList.stream() .collect(summingDouble(String::length));
3.11. Collectors.maxBy()/minBy()
MaxBy/MinBy collectors return the biggest/the smallest element of a Stream according to a provided Comparator instance.
We can pick the biggest element by doing:
Optional<String> result = givenList.stream() .collect(maxBy(Comparator.naturalOrder()));
Notice that returned value is wrapped in an Optional instance. This forces users to rethink the empty collection cornercase.
3.12. Collectors.groupingBy()
GroupingBy collector is used for grouping objects by some property and storing results in a Map instance.
We can group them by string length and store grouping results in Set instances:
Map<Integer, Set<String>> result = givenList.stream() .collect(groupingBy(String::length, toSet()));
This will result in following being true:
assertThat(result) .containsEntry(1, newHashSet("a", "b")) .containsEntry(2, newHashSet("bb")) .containsEntry(3, newHashSet("ccc")) .containsEntry(4, newHashSet("dd"));
Notice that the second argument of the groupingBy method is actually a Collector and you are free to use any Collector of your choice.
3.13. Collectors.partitioningBy
PartitioningBy is a specialized case of groupingBy that accepts a Predicate instance and collects Stream elements into a Map instance that stores Boolean values as keys and collections as values. Under the “true” key, you can find a collection of elements matching the given Predicate and under the “false” key, you can find a collection of elements not matching the given Predicate.
You can write:
Map<Boolean, List<String>> result = givenList.stream() .collect(partitioningBy(s -> s.length() > 2))
Which results in a Map containing:
{false=["a", "bb", "dd"], true=["ccc"]}
4. Custom Collectors
If you want to write your own Collector implementation, you need to implement Collector interface and specify its 3 generic parameters:
public interface Collector<T, A, R> {...}
- T – the type of objects that will be available for collection,
- A – the type of an mutable accumulator object,
- R – the type of a final result.
Let’s write an example Collector for collecting elements into an ImmutableSet instance. We start by specifying the right types:
private class ImmutableSetCollector<T> implements Collector<T, ImmutableSet.Builder<T>, ImmutableSet<T>> {...}
Since we need a mutable collection for internal collection operation handling, we can’t use ImmutableSet for this, we need to use some other mutable collection or any other class that could temporarily accumulate objects for us.
In this case we will go on with a ImmutableSet.Builder and now we need to implement 5 methods:
- Supplier<ImmutableSet.Builder<T>> supplier()
- BiConsumer<ImmutableSet.Builder<T>, T> accumulator()
- BinaryOperator<ImmutableSet.Builder<T>> combiner()
- Function<ImmutableSet.Builder<T>, ImmutableSet<T>> finisher()
- Set<Characteristics> characteristics()
supplier() method returns a Supplier instance that generates an empty accumulator instance, so in this case we can simply write:
@Override public Supplier<ImmutableSet.Builder<T>> supplier() { return ImmutableSet::builder; }
accumulator() method returns a function that is used for adding a new element to an existing accumulator object, so let’s just use the Builder‘s add method.
@Override public BiConsumer<ImmutableSet.Builder<T>, T> accumulator() { return ImmutableSet.Builder::add; }
combiner() method returns a function that is used for merging two accumulators together:
@Override public BinaryOperator<ImmutableSet.Builder<T>> combiner() { return (left, right) -> left.addAll(right.build()); }
finisher() method returns a function that is used for converting an accumulator to final result type, so in this case we will just use Builder‘s build method:
@Override public Function<ImmutableSet.Builder<T>, ImmutableSet<T>> finisher() { return ImmutableSet.Builder::build; }
characteristics() method is used to provide Stream with some additional information that will be used for internal optimizations. In this case we do not pay attention to the elements order in a Set, so we will use Characteristics.UNORDERED. In order to obtain more information regarding this subject, check Characteristics‘ JavaDoc.
@Override public Set<Characteristics> characteristics() { return Sets.immutableEnumSet(Characteristics.UNORDERED); }
Here is the complete implementation along with the usage:
public class ImmutableSetCollector<T> implements Collector<T, ImmutableSet.Builder<T>, ImmutableSet<T>> { @Override public Supplier<ImmutableSet.Builder<T>> supplier() { return ImmutableSet::builder; } @Override public BiConsumer<ImmutableSet.Builder<T>, T> accumulator() { return ImmutableSet.Builder::add; } @Override public BinaryOperator<ImmutableSet.Builder<T>> combiner() { return (left, right) -> left.addAll(right.build()); } @Override public Function<ImmutableSet.Builder<T>, ImmutableSet<T>> finisher() { return ImmutableSet.Builder::build; } @Override public Set<Characteristics> characteristics() { return Sets.immutableEnumSet(Characteristics.UNORDERED); } public static <T> ImmutableSetCollector<T> toImmutableSet() { return new ImmutableSetCollector<>(); }
and here in action:
List<String> givenList = Arrays.asList("a", "bb", "ccc", "dddd"); ImmutableSet<String> result = givenList.stream() .collect(toImmutableSet());
5. Conclusion
In this article we explored in depth Java 8’s collectors and showed how to implement a custom collector.
All code examples are available on the GitHub.