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

Слияние двух карт с Java 8

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

1. Введение

В этом кратком руководстве мы покажем, как объединить две карты, используя возможности Java 8 .

Чтобы быть более конкретным, мы рассмотрим различные сценарии слияния, включая карты с повторяющимися записями.

2. Инициализация

Для начала давайте определим два экземпляра карты :

private static Map<String, Employee> map1 = new HashMap<>();
private static Map<String, Employee> map2 = new HashMap<>();

Класс Сотрудник выглядит следующим образом:

public class Employee {
 
    private Long id;
    private String name;
 
    // constructor, getters, setters
}

Затем мы можем передать некоторые данные в экземпляры карты :

Employee employee1 = new Employee(1L, "Henry");
map1.put(employee1.getName(), employee1);
Employee employee2 = new Employee(22L, "Annie");
map1.put(employee2.getName(), employee2);
Employee employee3 = new Employee(8L, "John");
map1.put(employee3.getName(), employee3);

Employee employee4 = new Employee(2L, "George");
map2.put(employee4.getName(), employee4);
Employee employee5 = new Employee(3L, "Henry");
map2.put(employee5.getName(), employee5);

Обратите внимание, что у нас есть идентичные ключи для записей employee1 и employee5 в наших картах, которые мы будем использовать позже.

3. Карта.слияние()

Java 8 добавляет новую функцию merge() в интерфейс java.util.Map .

Вот как работает функция merge() : если указанный ключ еще не связан со значением или значение равно null, он связывает ключ с заданным значением.

В противном случае он заменяет значение результатами данной функции переназначения. Если результат функции переназначения равен нулю, он удаляет результат.

Во-первых, давайте создадим новый HashMap , скопировав все записи из map1 :

Map<String, Employee> map3 = new HashMap<>(map1);

Далее давайте представим функцию merge() вместе с правилом слияния:

map3.merge(key, value, (v1, v2) -> new Employee(v1.getId(),v2.getName())

Наконец, мы пройдемся по map2 и объединим записи в map3 :

map2.forEach(
(key, value) -> map3.merge(key, value, (v1, v2) -> new Employee(v1.getId(),v2.getName())));

Давайте запустим программу и распечатаем содержимое map3 :

John=Employee{id=8, name='John'}
Annie=Employee{id=22, name='Annie'}
George=Employee{id=2, name='George'}
Henry=Employee{id=1, name='Henry'}

В результате наша объединенная карта имеет все элементы предыдущих записей HashMap . Записи с повторяющимися ключами были объединены в одну запись .

Кроме того, мы замечаем, что объект Employee последней записи имеет идентификатор из map1 , а значение выбирается из map2 .

Это из-за правила, которое мы определили в нашей функции слияния:

(v1, v2) -> new Employee(v1.getId(), v2.getName())

4. Поток.concat()

Stream API в Java 8 также может предоставить простое решение нашей проблемы. Во- первых, нам нужно объединить наши экземпляры Map в один Stream . Это именно то, что делает операция Stream.concat() :

Stream combined = Stream.concat(map1.entrySet().stream(), map2.entrySet().stream());

Здесь мы передаем наборы записей карты в качестве параметров. Далее нам нужно собрать наш результат в новую карту . Для этого мы можем использовать Collectors.toMap() :

Map<String, Employee> result = combined.collect(
Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue));

В результате сборщик будет использовать существующие ключи и значения наших карт. Но это решение далеко от совершенства. Как только наш сборщик встретит записи с повторяющимися ключами, он выдаст исключение IllegalStateException .

Чтобы справиться с этой проблемой, мы просто добавляем в наш сборщик третий лямбда-параметр «объединения»:

(value1, value2) -> new Employee(value2.getId(), value1.getName())

Он будет использовать лямбда-выражение каждый раз, когда будет обнаружен дубликат ключа.

Наконец, собираем все вместе:

Map<String, Employee> result = Stream.concat(map1.entrySet().stream(), map2.entrySet().stream())
.collect(Collectors.toMap(
Map.Entry::getKey,
Map.Entry::getValue,
(value1, value2) -> new Employee(value2.getId(), value1.getName())));

Наконец, давайте запустим код и посмотрим на результаты:

George=Employee{id=2, name='George'}
John=Employee{id=8, name='John'}
Annie=Employee{id=22, name='Annie'}
Henry=Employee{id=3, name='Henry'}

Как мы видим, повторяющиеся записи с ключом «Генри» были объединены в новую пару «ключ-значение», где идентификатор нового сотрудника был выбран из карты2, а значение — из карты1 .

5. Поток()

Чтобы продолжать использовать Stream API, мы можем превратить наши экземпляры Map в единый поток с помощью Stream.of() .

Здесь нам не нужно создавать дополнительную коллекцию для работы с потоками:

Map<String, Employee> map3 = Stream.of(map1, map2)
.flatMap(map -> map.entrySet().stream())
.collect(Collectors.toMap(
Map.Entry::getKey,
Map.Entry::getValue,
(v1, v2) -> new Employee(v1.getId(), v2.getName())));

Сначала мы преобразуем map1 и map2 в один поток . Далее мы конвертируем поток в карту. Как мы видим, последний аргумент toMap() — это функция слияния. Он решает проблему дублирующихся ключей, выбирая поле id из записи v1 и имя из v2 .

Распечатанный экземпляр map3 после запуска программы:

George=Employee{id=2, name='George'}
John=Employee{id=8, name='John'}
Annie=Employee{id=22, name='Annie'}
Henry=Employee{id=1, name='Henry'}

6. Простое потоковое вещание

Кроме того, мы можем использовать конвейер stream() для сборки наших записей карты. Фрагмент кода ниже демонстрирует, как добавить записи из map2 и map1 , игнорируя повторяющиеся записи:

Map<String, Employee> map3 = map2.entrySet()
.stream()
.collect(Collectors.toMap(
Map.Entry::getKey,
Map.Entry::getValue,
(v1, v2) -> new Employee(v1.getId(), v2.getName()),
() -> new HashMap<>(map1)));

Как мы ожидаем, результаты после слияния:

{John=Employee{id=8, name='John'}, 
Annie=Employee{id=22, name='Annie'},
George=Employee{id=2, name='George'},
Henry=Employee{id=1, name='Henry'}}

7. СтримЭкс

Помимо решений, предоставляемых JDK, мы также можем использовать популярную библиотеку StreamEx .

Проще говоря, StreamEx является усовершенствованием Stream API и предоставляет множество дополнительных полезных методов. Мы будем использовать экземпляр EntryStream для работы с парами ключ-значение :

Map<String, Employee> map3 = EntryStream.of(map1)
.append(EntryStream.of(map2))
.toMap((e1, e2) -> e1);

Идея состоит в том, чтобы объединить потоки наших карт в один. Затем мы собираем записи в новый экземпляр map3 . Важно отметить, что выражение (e1, e2) -> e1 помогает определить правило для работы с повторяющимися ключами. Без него наш код выдаст исключение IllegalStateException .

А теперь результаты:

{George=Employee{id=2, name='George'}, 
John=Employee{id=8, name='John'},
Annie=Employee{id=22, name='Annie'},
Henry=Employee{id=1, name='Henry'}}

8. Резюме

В этой короткой статье мы узнали о различных способах слияния карт в Java 8. В частности, мы использовали Map.merge(), Stream API, библиотеку StreamEx .

Как всегда, код, использованный во время обсуждения, можно найти на GitHub .