1. 概述

本文我们将学习 Collectors.toMap() 的用法,并演示如何将一个 List 类型转为 Map。

2. List to Map

首先我们定义一个 Book class:

class Book {
    private String name;
    private int releaseYear;
    private String isbn;

    // getters and setters
}

创建bookList,并插入测试数据:

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"));

toMap 有多个重载方法,这里我们用到:

Collector<T, ?, Map<K,U>> toMap(Function<? super T, ? extends K> keyMapper,
  Function<? super T, ? extends U> valueMapper)

*使用 toMap,需要知道目标map Key和Value的映射策略*

这里我们将isbn作为Map的Key,name作为Map的Value

public Map<String, String> listToMap(List<Book> books) {
    return books.stream().collect(Collectors.toMap(Book::getIsbn, Book::getName));
}

单元测试:

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

3. 解决 Key 冲突问题

上面的例子工作的很好,但如果有重复的键会发生什么呢?

假设我们使用每本书的发行年份(releaseYear)作为键名:

public Map<Integer, Book> listToMapWithDupKeyError(List<Book> books) {
    return books.stream().collect(
      Collectors.toMap(Book::getReleaseYear, Function.identity()));
}

结果抛出 IllegalStateException 异常:

@Test(expected = IllegalStateException.class)
public void whenMapHasDuplicateKey_without_merge_function_then_runtime_exception() {
    convertToMap.listToMapWithDupKeyError(bookList);
}

为了解决这个问题,我们将使用带有 mergeFunction 参数的重载方法:

Collector<T, ?, M> toMap(Function<? super T, ? extends K> keyMapper,
  Function<? super T, ? extends U> valueMapper,
  BinaryOperator<U> mergeFunction)

我们引入一了个合并函数,在发生冲突的情况下,我们将保留已有的记录:

public Map<Integer, Book> listToMapWithDupKey(List<Book> books) {
    return books.stream().collect(Collectors.toMap(Book::getReleaseYear, Function.identity(),
      (existing, replacement) -> existing));
}

也就是说,键名重复时,新的数据不会覆盖旧的。下面是测试用例:

@Test
public void whenMapHasDuplicateKeyThenMergeFunctionHandlesCollision() {
    Map<Integer, Book> booksByYear = convertToMap.listToMapWithDupKey(bookList);
    assertEquals(2, booksByYear.size());
    assertEquals("0395489318", booksByYear.get(1954).getIsbn());
}

4. 其他 Map 类型

toMap() 方法默认返回的是 HashMap.

当然我们也可返回不同的 Map 实现:

Collector<T, ?, M> toMap(Function<? super T, ? extends K> keyMapper,
  Function<? super T, ? extends U> valueMapper,
  BinaryOperator<U> mergeFunction,
  Supplier<M> mapSupplier)

其中 mapSupplier 函数返回一个新的、空的 Map

4.1. ListConcurrentMap

下面我们利用 mapSupplier 返回一个 ConcurrentHashMap

public Map<Integer, Book> listToConcurrentMap(List<Book> books) {
    return books.stream().collect(Collectors.toMap(Book::getReleaseYear, Function.identity(),
      (o1, o2) -> o1, ConcurrentHashMap::new));
}

测试代码:

@Test
public void whenCreateConcurrentHashMap() {
    assertTrue(convertToMap.listToConcurrentMap(bookList) instanceof ConcurrentHashMap);
}

4.2. Sorted Map

最后,让我们看看如何返回排序后的map。 为此,我们将使用 TreeMap 作为 mapSupplier 参数。

默认情况下 TreeMap 根据其键的自然顺序进行排序,所以我们不必自己显式对 books 进行排序:

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

因此在我们的例子中,返回的 TreeMap 将按书名的字母顺序排序:

@Test
public void whenMapisSorted() {
    assertTrue(convertToMap.listToSortedMap(bookList).firstKey().equals(
      "The Fellowship of the Ring"));
}

5. 总结

在本文中,我们研究了 Collectors 类的 toMap() 方法。 它允许我们从 Stream 创建一个新的 Map

我们还学习了如何解决关键冲突并创建不同的map实现。

最后, 本文中的代码可在 GitHub 上找到.