1. Overview
In this quick tutorial, we're going to talk about the toMap() method of the Collectors class. We'll use it to collect Streams into a Map instance.
For all the examples covered here, we'll use a list of books as a starting point and transform it into different Map implementations.
The article discusses Java 8 Collectors, showing examples of built-in collectors, as well as showing how to build custom collector.
Learn how to collect Java Streams to immutable Collections.
In this article, we explore new Stream collectors that were introduced in JDK 9
2. List to Map
We'll start with the simplest case, by transforming a List into a Map.
Here is how we define our Book class:
class Book {
private String name;
private int releaseYear;
private String isbn;
// getters and setters
}
And we'll create a list of books to validate our code:
List<Book> bookList = new ArrayList<>();
bookList.add(new Book("The Fellowship of the Ring", 1954, "0395489318"));
bookList.add(new Book("The Two Towers", 1954, "0345339711"));
bookList.add(new Book("The Return of the King", 1955, "0618129111"));
For this scenario we'll use the following overload of the toMap() method:
Collector<T, ?, Map<K,U>> toMap(Function<? super T, ? extends K> keyMapper,
Function<? super T, ? extends U> valueMapper)
With toMap, we can indicate strategies for how to get the key and value for the map:
public Map<String, String> listToMap(List<Book> books) {
return books.stream().collect(Collectors.toMap(Book::getIsbn, Book::getName));
}
And we can easily validate that it works:
@Test
public void whenConvertFromListToMap() {
assertTrue(convertToMap.listToMap(bookList).size() == 3);
}
3. Solving Key Conflicts
The example above worked well, but what would happen with a duplicate key?
Let's imagine that we keyed our Map by each Book‘s release year:
public Map<Integer, Book> listToMapWithDupKeyError(List<Book> books) {
return books.stream().collect(
Collectors.toMap(Book::getReleaseYear, Function.identity()));
}
Given our earlier list of books, we'd see an IllegalStateException:
@Test(expected = IllegalStateException.class)
public void whenMapHasDuplicateKey_without_merge_function_then_runtime_exception() {
convertToMap.listToMapWithDupKeyError(bookList);
}
To resolve it, we need to use a different method with an additional parameter, the mergeFunction:
Collector<T, ?, M> toMap(Function<? super T, ? extends K> keyMapper,
Function<? super T, ? extends U> valueMapper,
BinaryOperator<U> mergeFunction)
Let's introduce a merge function that indicates that, in the case of a collision, we keep the existing entry:
public Map<Integer, Book> listToMapWithDupKey(List<Book> books) {
return books.stream().collect(Collectors.toMap(Book::getReleaseYear, Function.identity(),
(existing, replacement) -> existing));
}
Or in other words, we get first-win behavior:
@Test
public void whenMapHasDuplicateKeyThenMergeFunctionHandlesCollision() {
Map<Integer, Book> booksByYear = convertToMap.listToMapWithDupKey(bookList);
assertEquals(2, booksByYear.size());
assertEquals("0395489318", booksByYear.get(1954).getIsbn());
}
4. Other Map Types
By default, a toMap() method will return a HashMap.
But we can return different Map implementations:
Collector<T, ?, M> toMap(Function<? super T, ? extends K> keyMapper,
Function<? super T, ? extends U> valueMapper,
BinaryOperator<U> mergeFunction,
Supplier<M> mapSupplier)
where the mapSupplier is a function that returns a new, empty Map with the results.
4.1. List to ConcurrentMap
Let's take the same example and add a mapSupplier function to return a ConcurrentHashMap:
public Map<Integer, Book> listToConcurrentMap(List<Book> books) {
return books.stream().collect(Collectors.toMap(Book::getReleaseYear, Function.identity(),
(o1, o2) -> o1, ConcurrentHashMap::new));
}
We'll go on and test our code:
@Test
public void whenCreateConcurrentHashMap() {
assertTrue(convertToMap.listToConcurrentMap(bookList) instanceof ConcurrentHashMap);
}
4.2. Sorted Map
Lastly, let's see how to return a sorted map. For that, we'll use a TreeMap as a mapSupplier parameter.
Because a TreeMap is sorted according to the natural ordering of its keys by default, we don't have to explicitly sort the books ourselves:
public TreeMap<String, Book> listToSortedMap(List<Book> books) {
return books.stream()
.collect(
Collectors.toMap(Book::getName, Function.identity(), (o1, o2) -> o1, TreeMap::new));
}
So in our case, the returned TreeMap will be sorted in alphabetical order by the book name:
@Test
public void whenMapisSorted() {
assertTrue(convertToMap.listToSortedMap(bookList).firstKey().equals(
"The Fellowship of the Ring"));
}
5. Conclusion
In this article, we looked into the toMap() method of the Collectors class. It allows us to create a new Map from a Stream.
We also learned how to resolve key conflicts and create different map implementations.
As always, the code is available over on GitHub.
res – REST with Spring (eBook) (everywhere)