Java Top

I just announced the new Learn Spring course, focused on the fundamentals of Spring 5 and Spring Boot 2:

>> CHECK OUT THE COURSE

1. Introduction

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 in here, we’ll use a list of books as a starting point and transform it into different Map implementations.

2. List to Map

We’ll start with the simplest case, by transforming a List into a Map.

Our Book class is defined as:

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 it works with:

@Test
public void whenConvertFromListToMap() {
    assertTrue(convertToMap.listToMap(bookList).size() == 3);
}

3. Solving Key Conflicts

The example above worked well, but what would happen if there’s 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 can we return different Map implementations? The answer is yes:

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 as above 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));
}

Let’s 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 need to sort a list and use a TreeMap as a mapSupplier parameter:

public TreeMap<String, Book> listToSortedMap(List<Book> books) {
    return books.stream() 
      .sorted(Comparator.comparing(Book::getName))
      .collect(Collectors.toMap(Book::getName, Function.identity(), (o1, o2) -> o1, TreeMap::new));
}

The code above will sort the list based on the book name and then collect the results to a TreeMap:

@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.

Java bottom

I just announced the new Learn Spring course, focused on the fundamentals of Spring 5 and Spring Boot 2:

>> CHECK OUT THE COURSE