Terminal operation either returns one value (of the same or another type than the input type) or does not return anything at all (produces only side effects). It does not allow another operation to be applied afterward and closes the stream.
In this post, we will continue covering the last of the terminal operations called collect():
R collect(Collector<T,A,R> collector)
It is a specialization of the reduce() operation. It allows implementing a vast variety of algorithms using the ready-to-use collectors from the java.util.stream.Collectors class. We discussed how to create a custom collector in Java streams 25. Collect 1. Custom collector. In this article, we will use only the collectors produced by the Collectors class.
Creating Map object using Collectors.toMap() collector
Warning! The Collectors.toMap() collector is not concurrent. For parallel streams, the combiner function operates by merging the keys from one map into another, which can be an expensive operation. If it is not required that results are merged into the Map in the encountered order, a better parallel processing performance may be achieved using Collectors.toConcurrentMap() or Collectors.groupingByConcurrent() collector which we are going to discuss in the following articles.
There are several overloaded versions of factory methods that create the Collectors.toMap() collector:
— Collector<T,?,Map<K,U>> toMap(Function<T,K> keyMapper, Function<T,U> valueMapper) – returns a Collector that accumulates elements into a Map whose keys and values are the result of applying the provided mapping functions to the input elements;
— Collector<T,?,Map<K,U>> toMap(Function<T,K> keyMapper, Function<T,U> valueMapper, BinaryOperator<U> mergeFunction) – returns a Collector that accumulates elements into a Map whose keys and values are the result of applying the provided mapping functions to the input elements;
— Collector<T,?,M> toMap(Function<T,K> keyMapper, Function<T,U> valueMapper, BinaryOperator<U> mergeFunction, Supplier<M> mapSupplier) – returns a Collector that accumulates elements into a Map whose keys and values are the result of applying the provided mapping functions to the input elements.
The keyMapper function extracts from the element the value that is going to serve as a key in the resulting Map. The valueMapper is the function that extracts value from the stream element that is going to be used as a value mapped to the key.
The mergeFunction is used when two values are matched to the same key, so it combines such values to resolve the clash.
The mapSupplier is used when a specific implementation of Map interface is required.
The following are the examples of the Collectors.toMap() usage:
Map<String, Integer> map =
Stream.of("cat", "fish", "hedgehog")
.collect(Collectors
.toMap(s -> s, s -> s.length()));
System.out.print(map);
//prints: {hedgehog=8, cat=3, fish=4}
As you can see, in this example we used the emitted element itself to serve as a key (s -> s).
Alternatively, we use the identity function that is by definition always returns the input element:
Map<String, Integer> map =
Stream.of("cat", "fish", "hedgehog")
.collect(Collectors
.toMap(Function.identity(),
String::length));
System.out.print(map);
//prints: {hedgehog=8, cat=3, fish=4}
The value in both cases is the string length.
Now let us add to the stream a duplicate element:
try {
Map<String, Integer> map =
Stream.of("cat", "fish", "cat")
.collect(Collectors
.toMap(Function.identity(),
String::length));
} catch (Exception ex){
System.out.print(ex.getMessage());
//prints: Duplicate key cat (attempted merging values 3 and 3)
}
We can avoid exception by providing the mergeFunction (and using the second overloaded version of the toMap() factory method):
Map<String, Integer> map =
Stream.of("cat", "fish", "cat")
.collect(Collectors
.toMap(Function.identity(),
String::length,
(oldValue, nextValue) -> oldValue));
System.out.print(map); //prints: {fish=4, cat=3}
As you can see, we use function (oldValue, nextValue) -> oldValue) to resolve the conflict (by letting the old value be used).
Such an implementation may not be desirable in the case we would like to get an idea of how many dups were discovered. If that is the case, the following solution may be preferable:
Map<String, Integer> map =
Stream.of("cat", "fish", "cat")
.collect(Collectors
.toMap(Function.identity(),
String::length,
(oldValue, nextValue) -> oldValue + 100));
System.out.print(map); //prints: {fish=4, cat=103}
Assuming that none of the elements is longer than 100, the value 103 tells us that the key “cat” was encountered twice.
Finally, we can provide mapSupplier to return a specific Map implementation we need. For example, if we need the keys to be always sorted, we will request to put them in a TreeMap:
TreeMap<String, Integer> map =
Stream.of("cat", "fish", "cat", "cat")
.collect(Collectors
.toMap(Function.identity(),
String::length,
(oldValue, nextValue) -> oldValue + 100, TreeMap::new));
System.out.print(map); //prints: {cat=203, fish=4}
As you can see, now the keys are sorted. Incidentally, we have added another “cat” element to the stream and, sure enough, the resulting value for “cat” is 203, which means that the key “cat” was encountered 3 times.
In the next post, we will talk about creating a Map object using Collectors.toConcurrentMap() collector.
See other posts on Java 8 streams and posts on other topics.
You can also use navigation pages for Java stream related blogs:
— Java 8 streams blog titles
— Create stream
— Stream operations
— Stream operation collect()
The source code of all the code examples is here in GitHub.
Send your comments using the link Contact or in response to my newsletter.
If you do not receive the newsletter, subscribe via link Subscribe under Contact.