Как в Java 8 преобразовать карту <K, V> в другую карту <K, V> с помощью лямбда?

145

Я только начал смотреть на Java 8 и попробовать лямбды, я подумал, что попробую переписать очень простую вещь, которую написал недавно. Мне нужно превратить карту строки в столбец в другую карту строки в столбец, где столбец на новой карте является защитной копией столбца на первой карте. Столбец имеет конструктор копирования. Ближайшее, что у меня есть, это:

    Map<String, Column> newColumnMap= new HashMap<>();
    originalColumnMap.entrySet().stream().forEach(x -> newColumnMap.put(x.getKey(), new Column(x.getValue())));

но я уверен, что должен быть лучший способ сделать это, и я был бы признателен за совет.

annesadleir
источник

Ответы:

230

Вы можете использовать Коллекционер :

import java.util.*;
import java.util.stream.Collectors;

public class Defensive {

  public static void main(String[] args) {
    Map<String, Column> original = new HashMap<>();
    original.put("foo", new Column());
    original.put("bar", new Column());

    Map<String, Column> copy = original.entrySet()
        .stream()
        .collect(Collectors.toMap(Map.Entry::getKey,
                                  e -> new Column(e.getValue())));

    System.out.println(original);
    System.out.println(copy);
  }

  static class Column {
    public Column() {}
    public Column(Column c) {}
  }
}
Макдауэлл
источник
7
Я думаю, что важная (и немного противоречащая интуиции) вещь, которую следует отметить в приведенном выше, заключается в том, что преобразование происходит в сборщике, а не в операции потока map ()
Брайан Агнью
27
Map<String, Integer> map = new HashMap<>();
map.put("test1", 1);
map.put("test2", 2);

Map<String, Integer> map2 = new HashMap<>();
map.forEach(map2::put);

System.out.println("map: " + map);
System.out.println("map2: " + map2);
// Output:
// map:  {test2=2, test1=1}
// map2: {test2=2, test1=1}

Вы можете использовать этот forEachметод, чтобы делать то, что хотите.

Что вы там делаете:

map.forEach(new BiConsumer<String, Integer>() {
    @Override
    public void accept(String s, Integer integer) {
        map2.put(s, integer);     
    }
});

Что мы можем упростить до лямбда:

map.forEach((s, integer) ->  map2.put(s, integer));

И поскольку мы просто вызываем существующий метод, мы можем использовать ссылку на метод , которая дает нам:

map.forEach(map2::put);
Аррем
источник
8
Мне нравится, как вы точно объяснили, что здесь происходит за кулисами, это делает это намного яснее. Но я думаю, что это дает мне прямую копию моей исходной карты, а не новую карту, на которой значения были превращены в защитные копии. Правильно ли я понимаю, что причина, по которой мы можем просто использовать (map2 :: put), заключается в том, что в лямбда-выражение входят те же аргументы, например (s, integer), что и в методе put? Так что, чтобы сделать защитную копию, она должна быть originalColumnMap.forEach((string, column) -> newColumnMap.put(string, new Column(column)));или я могу ее сократить?
annesadleir
Да Вы. Если вы просто передаете те же аргументы, вы можете использовать ссылку на метод, однако в этом случае, поскольку у вас есть параметр new Column(column)в качестве параметра, вам придется пойти с этим.
Arrem
Мне этот ответ больше нравится, потому что он работает, если обе карты уже предоставлены вам, например, при обновлении сеанса.
Дэвид Стэнли
Не уверен, что понял смысл такого ответа. Он переписывает конструктор в основном всех реализаций Map из существующей Map - вот так .
Кинеолян
13

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

Map<String, Column> newColumnMap = originalColumnMap.clone();
newColumnMap.replaceAll((s, c) -> new Column(c));
Левентов
источник
Это очень удобный и краткий способ сделать это. Похоже, что и HashMap.clone () есть Map<String,Column> newColumnMap = new HashMap<>(originalColumnMap);. Не знаю, под одеялом ли что-то по-другому.
annesadleir
2
Преимущество этого подхода заключается в том, что он сохраняет Mapповедение реализации, т. Е. Если Mapесть EnumMapили SortedMap, результат Mapтакже будет. В случае SortedMapсо специальным значением Comparatorэто может иметь огромное смысловое различие. Ну что ж, то же самое относится к IdentityHashMap
Хольгер
8

Сохраняйте простоту и используйте Java 8: -

 Map<String, AccountGroupMappingModel> mapAccountGroup=CustomerDAO.getAccountGroupMapping();
 Map<String, AccountGroupMappingModel> mapH2ToBydAccountGroups = 
              mapAccountGroup.entrySet().stream()
                         .collect(Collectors.toMap(e->e.getValue().getH2AccountGroup(),
                                                   e ->e.getValue())
                                  );
Анант Хурана
источник
в данном случае: map.forEach (map2 :: put); просто :)
ses
6

Если вы используете в своем проекте Guava (минимум v11), вы можете использовать Maps :: transformValues .

Map<String, Column> newColumnMap = Maps.transformValues(
  originalColumnMap,
  Column::new // equivalent to: x -> new Column(x) 
)

Примечание: значения этой карты оцениваются лениво. Если преобразование обходится дорого, вы можете скопировать результат на новую карту, как это предлагается в документации Guava.

To avoid lazy evaluation when the returned map doesn't need to be a view, copy the returned map into a new map of your choosing.
Андреа Бергонцо
источник
Примечание. Согласно документам, это возвращает ленивое оцененное представление для originalColumnMap, поэтому функция Column :: new переоценивается каждый раз при доступе к записи (что может быть нежелательно, если функция сопоставления дорогая)
Erric
Верный. Если преобразование обходится дорого, вам, вероятно, не страшны накладные расходы, связанные с копированием его на новую карту, как это предлагается в документации. To avoid lazy evaluation when the returned map doesn't need to be a view, copy the returned map into a new map of your choosing.
Андреа Бергонцо
Верно, но, возможно, стоит добавить в ответ в качестве сноски для тех, кто склонен пропускать чтение документации.
Erric
@Erric: В этом есть смысл, только что добавил.
Андреа Бергонцо
3

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

Map<String, Integer> pointsByName = new HashMap<>();
Map<String, Integer> maxPointsByName = new HashMap<>();

Map<String, Double> gradesByName = pointsByName.entrySet().stream()
        .map(entry -> new AbstractMap.SimpleImmutableEntry<>(
                entry.getKey(), ((double) entry.getValue() /
                        maxPointsByName.get(entry.getKey())) * 100d))
        .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue));
Лукас Росс
источник
1

Если вы не против использования сторонних библиотек, моя cyclops-react lib имеет расширения для всех типов JDK Collection , включая Map . Вы можете напрямую использовать методы map или bimap для преобразования вашей карты. MapX может быть построен из существующей карты, например.

  MapX<String, Column> y = MapX.fromMap(orgColumnMap)
                               .map(c->new Column(c.getValue());

Если вы также хотите изменить ключ, вы можете написать

  MapX<String, Column> y = MapX.fromMap(orgColumnMap)
                               .bimap(this::newKey,c->new Column(c.getValue());

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

Поскольку MapX расширяет Map, сгенерированная карта также может быть определена как

  Map<String, Column> y
Джон МакКлин
источник