Перейти к основному содержимому

Коллекторы Java 8 toMap

· 4 мин. чтения

Задача: Сумма двух чисел

Напишите функцию twoSum. Которая получает массив целых чисел nums и целую сумму target, а возвращает индексы двух чисел, сумма которых равна target. Любой набор входных данных имеет ровно одно решение, и вы не можете использовать один и тот же элемент дважды. Ответ можно возвращать в любом порядке...

ANDROMEDA

1. Обзор

В этом кратком руководстве мы поговорим о методе toMap() класса Collectors . Мы будем использовать его для сбора Stream в экземпляр Map .

Для всех рассмотренных здесь примеров мы будем использовать список книг в качестве отправной точки и преобразовывать его в различные реализации карты .

2. Список на карту

Мы начнем с самого простого случая, превратив List в Map .

Вот как мы определяем наш класс Book :

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

// getters and setters
}

И мы создадим список книг для проверки нашего кода:

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 мы можем указать стратегии получения ключа и значения для карты :

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. Разрешение ключевых конфликтов

Приведенный выше пример работал хорошо, но что произойдет с дубликатом ключа?

Давайте представим, что мы связали нашу Карту с каждым годом выпуска Книги :

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

Чтобы решить эту проблему, нам нужно использовать другой метод с дополнительным параметром, функцию слияния :

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

Или, другими словами, мы получаем поведение first-win:

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

4. Другие типы карт

По умолчанию метод 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 — это функция, которая возвращает новую пустую карту с результатами.

4.1. Список в ConcurrentMap

Давайте возьмем тот же пример и добавим функцию 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. Отсортированная карта

Наконец, давайте посмотрим, как вернуть отсортированную карту. Для этого мы будем использовать TreeMap в качестве параметра mapSupplier .

Поскольку TreeMap по умолчанию сортируется в соответствии с естественным порядком ключей, нам не нужно явно сортировать книги самостоятельно:

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. Вывод

В этой статье мы рассмотрели метод toMap () класса Collectors . Это позволяет нам создать новую карту из потока .

Мы также научились разрешать ключевые конфликты и создавать различные реализации карт.

Как всегда, код доступен на GitHub .