Stream.collect() is one of the endpoint methods of the Stream API in Java 8. It allows us to perform a mutable folding of data elements held in the Stream instance (repackaging elements into some data structure, applying some additional logic, concatenating data, and so on). The specific policy for this operation is provided through the implementation of the Collector interface.
Collectors
All predefined implementations can be found in the _Collectors_ class. It is common practice to combine the following static imports with these methods to improve readability:
import static java.util.stream.Collectors.*;
Copy the code
Or import the collector you need individually:
import static java.util.stream.Collectors.toList;
import static java.util.stream.Collectors.toMap;
import static java.util.stream.Collectors.toSet;
Copy the code
The following list is used in the examples that follow:
List<String> givenList = Arrays.asList("a"."bb"."ccc"."dd");
Copy the code
Collectors.toList()
The toList collector can be used to collect all Stream elements into a List instance. Note that we can use this method without assuming any particular List implementation. If you want to control this, use the toCollection method. We start by creating a stream instance that contains a List of elements and collecting the elements into a List instance:
List<String> result = givenList.stream()
.collect(toList());
Copy the code
Java 10 has introduced a convenient way to collect elements from a Stream into an unmodifiable List:
List<String> result = givenList.stream()
.collect(toUnmodifiableList());
assertThatThrownBy(() -> result.add("foo")).isInstanceOf(UnsupportedOperationException.class);
Copy the code
Collectors.toSet()
The toSet collector can be used to collect all Stream elements into a Set instance. Similarly, we can use this method without assuming any particular Set implementation. If you want to control this, use the toCollection method. Let’s collect elements from the stream into a Set instance:
Set<String> result = givenList.stream()
.collect(toSet());
Copy the code
The Set contains no duplicate elements, and if the Set contains equal elements, the final Set appears only once:
List<String> listWithDuplicates = Arrays.asList("a"."bb"."c"."d"."bb");
Set<String> result = listWithDuplicates.stream().collect(toSet());
assertThat(result).hasSize(4);
Copy the code
The _toUnmodifiableSet()_ method can be used to create unmodifiable sets:
Set<String> result = givenList.stream()
.collect(toUnmodifiableSet());
assertThatThrownBy(() -> result.add("foo")).isInstanceOf(UnsupportedOperationException.class);
Copy the code
Collectors.toCollection()
As mentioned earlier, when using the toSet and toList collectors, you cannot make any assumptions about their implementation. If you want to specify an implementation type, you need to use a toCollection collector. Let’s collect elements from the stream into an instance of LinkedList:
List<String> result = givenList.stream()
.collect(toCollection(LinkedList::new))
Copy the code
Note that this does not apply to any immutable set. In this case, you will need to write a custom Collector implementation or use collectionAndThen.
Collectors.toMap()
The toMap collector can be used to collect stream elements into a Map instance. To do this, we need to provide two functions:
- keyMapper
- valueMapper
KeyMapper is used to extract mapping keys from a stream element, and valueMapper is used to extract values associated with a given key. Let’s gather these elements into a Map that uses strings as keys and stores their lengths as values:
Map<String, Integer> result = givenList.stream()
.collect(toMap(Function.identity(), String::length))
Copy the code
Function.identity() is just a shortcut to define functions that take the same arguments and return values. What happens if our collection contains repeating elements? Unlike toSet, the toMap operation does not silently filter duplicates. This is understandable — how should the program determine which value to select for this key?
List<String> listWithDuplicates = Arrays.asList("a"."bb"."c"."d"."bb");
assertThatThrownBy(() -> {
listWithDuplicates.stream().collect(toMap(Function.identity(), String::length));
}).isInstanceOf(IllegalStateException.class);
Copy the code
Note that toMap doesn’t even evaluate whether the values are equal. If you see a duplicate key, an IllegalStateException is immediately raised. In case of key collisions, we should use another form of toMap:
Map<String, Integer> result = givenList.stream()
.collect(toMap(Function.identity(), String::length, (item, identicalItem) -> item));
Copy the code
The third argument here is BinaryOperator, where we can specify how to handle conflicts. In the scenario described above, we can choose either of these conflicting values because we know that the same string will always have the same length. Like List and Set, Java 10 introduces a simple way to collect Stream elements into unmodifiable maps:
Map<String, Integer> result = givenList.stream()
.collect(toMap(Function.identity(), String::length));
assertThatThrownBy(() -> result.put("foo".3))
.isInstanceOf(UnsupportedOperationException.class);
Copy the code
Collectors.c_ollectingAndThen()_
Collingandthen is a special collector that allows another operation to be performed on the result directly after the collection. Let’s collect the stream elements into a List instance and convert the result to an ImmutableList instance:
List<String> result = givenList.stream()
.collect(collectingAndThen(toList(), ImmutableList::copyOf))
Copy the code
Collectors.j_oining()_
A Joining collector can be used to join Stream elements. We can add them together in the following ways:
String result = givenList.stream()
.collect(joining());
// Result is "abbcccdd"
Copy the code
You can also specify custom delimiters, prefixes, suffixes:
String result = givenList.stream()
.collect(joining(""."PRE-"."-POST"));
// result is "pre-a BB CCC DD-post"
Copy the code
Mathematical class collector
The Counting collector is used to count elements in the flow.
Long result = givenList.stream()
.collect(counting());
Copy the code
SummarizingDouble/Long/Int is a special collector, it returns a special class, which contains information about the extraction of elements flow of digital data statistics:
DoubleSummaryStatistics result = givenList.stream()
.collect(summarizingDouble(String::length));
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);
Copy the code
AveragingDouble/Long/Int is a collector that returns the average value of an element’s attributes. For example, to calculate the average length of a string:
Double result = givenList.stream()
.collect(averagingDouble(String::length));
Copy the code
SummingDouble/Long/Int is a collector that returns the sum of the extracted elements. Compute the sum of string lengths:
Double result = givenList.stream()
.collect(summingDouble(String::length));
Copy the code
Comparison class collector
The MaxBy/MinBy collector returns the maximum/minimum element of the Stream based on the provided Comparator instance. The maximum element can be filtered by:
Optional<String> result = givenList.stream()
.collect(maxBy(Comparator.naturalOrder()));
Copy the code
Note that the return value is encapsulated as Optional, which forces the user to rethink boundary cases such as empty collections.
The GroupingBy collector is used to group objects according to certain properties and store the results in a Map instance. The following code groups the elements by string length and stores the grouping results into a Set:
Map<Integer, Set<String>> result = givenList.stream()
.collect(groupingBy(String::length, toSet()));
assertThat(result)
.containsEntry(1, newHashSet("a"))
.containsEntry(2, newHashSet("bb"."dd"))
.containsEntry(3, newHashSet("ccc"));
Copy the code
PartitioningBy is a special case of groupingBy, which takes a predicate instance and collects the Stream elements into a Map instance that has Boolean values as keys and grouped collections as values. Under the “true” key, you can find the set of elements that match a given predicate, and under the “false” key, you can find the set of elements that do not match a given predicate.
Map<Boolean, List<String>> result = givenList.stream()
.collect(partitioningBy(s -> s.length() > 2))
Copy the code
The content of the resulting Map is:
{false= ["a"."bb"."dd"].true= ["ccc"]}
Copy the code
Custom collector
If you want to customize the Collector implementation, you implement the Collector interface and specify its three generic parameters:
public interface Collector<T.A.R> {... }Copy the code
- T – The type of object that can be collected,
- A – Type of variable accumulator object,
- R – The type of the final result.
We can implement a collector that collects elements into _ImmutableSet_, first declaring generic types:
private class ImmutableSetCollector<T> implements Collector<T.ImmutableSet.Builder<T>, ImmutableSet<T>> {... }Copy the code
Since we need a mutable collection to handle collection operations inside the collector, we cannot use ImmutableSet. We need to use some other mutable collection or any class that can temporarily store objects. Here, we chose to use imMutableset. Builder, and now we need to implement 5 methods:
- Supplier<ImmutableSet.Builder> supplier(a)
- BiConsumer<ImmutableSet.Builder, T> accumulator(a)
- BinaryOperator<ImmutableSet.Builder> combiner(a)
- Function<ImmutableSet.Builder, ImmutableSet> finisher(a)
- Set characteristics(a)
The eastern () method returns a supplier instance that generates an empty Accumulator instance, so in this case we can simply return a Builder object. Accumulator () returns a function that adds a new element to the existing Accumulator object, so we can use Builder’s Add method. The combiner() method returns a function that is used to merge elements from two Accumulators together. The finisher() method returns a function to convert accumulator Accumulator to the final result type, so in this case, we’ll use the builder’s build() method. The characteristic() method is used to provide the flow with some additional information that will be used for internal optimization. In this case, we do not pay attention to the Set of elements in the order, so we can use Characteristics. The UNORDERED. The complete implementation is as follows:
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(a) {
return Sets.immutableEnumSet(Characteristics.UNORDERED);
}
public static <T> ImmutableSetCollector<T> toImmutableSet(a) {
return newImmutableSetCollector<>(); }}Copy the code
This custom collector can be tested:
List<String> givenList = Arrays.asList("a"."bb"."ccc"."dddd");
ImmutableSet<String> result = givenList.stream().collect(toImmutableSet());
Copy the code