Java8: от HashMap <X, Y> до HashMap <X, Z> с использованием Stream / Map-Reduce / Collector

210

Я знаю, как "преобразовать" простую Java List из Y-> Z, то есть:

List<String> x;
List<Integer> y = x.stream()
        .map(s -> Integer.parseInt(s))
        .collect(Collectors.toList());

Теперь я хотел бы сделать то же самое с картой, то есть:

INPUT:
{
  "key1" -> "41",    // "41" and "42"
  "key2" -> "42      // are Strings
}

OUTPUT:
{
  "key1" -> 41,      // 41 and 42
  "key2" -> 42       // are Integers
}

Решение не должно ограничиваться String-> Integer. Прямо как вList примере выше, я бы хотел вызвать любой метод (или конструктор).

Бенджамин М
источник

Ответы:

373
Map<String, String> x;
Map<String, Integer> y =
    x.entrySet().stream()
        .collect(Collectors.toMap(
            e -> e.getKey(),
            e -> Integer.parseInt(e.getValue())
        ));

Это не так хорошо, как список кода. Вы не можете создавать новые Map.Entrys в map()вызове, поэтому работа смешивается с collect()вызовом.

Джон Кугельман
источник
59
Вы можете заменить e -> e.getKey()на Map.Entry::getKey. Но это вопрос вкуса / стиля программирования.
Хольгер
5
На самом деле это вопрос производительности, вы предлагаете немного
Джон Бургин,
36

Вот несколько вариантов ответа Сотириоса Делиманолиса , который был довольно хорош для начала (+1). Учтите следующее:

static <X, Y, Z> Map<X, Z> transform(Map<? extends X, ? extends Y> input,
                                     Function<Y, Z> function) {
    return input.keySet().stream()
        .collect(Collectors.toMap(Function.identity(),
                                  key -> function.apply(input.get(key))));
}

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

Map<String, String> input = new HashMap<String, String>();
input.put("string1", "42");
input.put("string2", "41");
Map<CharSequence, Integer> output = transform(input, Integer::parseInt);

(Существует также пример значений карты, но он действительно надуманный, и я признаю, что использование ограниченного символа подстановки для Y помогает только в крайних случаях.)

Второй момент заключается в том, что вместо запуска потока над входной картой entrySetя запускал его через keySet. Это делает код немного чище, я думаю, за счет необходимости извлекать значения из карты, а не из записи карты. Между прочим, у меня изначально был key -> keyпервый аргумент, toMap()и по какой-то причине это не удалось с ошибкой вывода типа. Поменял его на (X key) -> keyработавший, как и сделал Function.identity().

Еще один вариант заключается в следующем:

static <X, Y, Z> Map<X, Z> transform1(Map<? extends X, ? extends Y> input,
                                      Function<Y, Z> function) {
    Map<X, Z> result = new HashMap<>();
    input.forEach((k, v) -> result.put(k, function.apply(v)));
    return result;
}

Это использует Map.forEach()вместо потоков. Это еще проще, я думаю, потому что это обходится без коллекционеров, которые несколько неуклюжи для использования с картами. Причина в том, что Map.forEach()ключ и значение задаются как отдельные параметры, тогда как поток имеет только одно значение - и вам нужно выбрать, использовать ли ключ или запись карты в качестве этого значения. С другой стороны, этому не хватает богатого, плавного совершенства других подходов. :-)

Стюарт Маркс
источник
11
Function.identity()может показаться классным, но поскольку первое решение требует поиска по карте / хешу для каждой записи, в то время как все остальные решения этого не делают, я бы не рекомендовал это делать.
Хольгер
13

Общее решение, как так

public static <X, Y, Z> Map<X, Z> transform(Map<X, Y> input,
        Function<Y, Z> function) {
    return input
            .entrySet()
            .stream()
            .collect(
                    Collectors.toMap((entry) -> entry.getKey(),
                            (entry) -> function.apply(entry.getValue())));
}

пример

Map<String, String> input = new HashMap<String, String>();
input.put("string1", "42");
input.put("string2", "41");
Map<String, Integer> output = transform(input,
            (val) -> Integer.parseInt(val));
Сотириос Делиманолис
источник
Хороший подход с использованием дженериков. Я думаю, что это может быть немного улучшено, хотя - см. Мой ответ.
Стюарт Маркс
13

Функция Guava - Maps.transformValuesэто то, что вы ищете, и она хорошо работает с лямбда-выражениями:

Maps.transformValues(originalMap, val -> ...)
Алекс Краусс
источник
Мне нравится такой подход, но будьте осторожны, чтобы не передавать его в функцию java.util.Function. Так как он ожидает com.google.common.base.Function, Eclipse выдает бесполезную ошибку - он говорит, что Function не применима для Function, что может сбивать с толку: «Метод transformValues ​​(Map <K, V1>, Function <? Super V1» , V2>) в типе Maps не применяется для аргументов (Map <Foo, Bar>, Function <Bar, Baz>) "
mskfisher
Если вы должны пройти java.util.Function, у вас есть два варианта. 1. Избегайте проблемы, используя лямбду, чтобы позволить выводу типа Java разобраться с этим. 2. Используйте ссылку на метод, такую ​​как javaFunction :: apply, чтобы создать новую лямбду, которую может определить вывод типа.
Джо
10

Это должно быть на 100% функционально и свободно? Если нет, то как насчет этого, который настолько короток, насколько это возможно:

Map<String, Integer> output = new HashMap<>();
input.forEach((k, v) -> output.put(k, Integer.valueOf(v));

( если вы можете жить со стыдом и чувством вины от объединения потоков с побочными эффектами )

Лукас Эдер
источник
5

Моя библиотека StreamEx, которая расширяет стандартный потоковый API, предоставляет EntryStreamкласс, который лучше подходит для преобразования карт:

Map<String, Integer> output = EntryStream.of(input).mapValues(Integer::valueOf).toMap();
Тагир Валеев
источник
4

Альтернатива, которая всегда существует для целей обучения, состоит в том, чтобы создать свой собственный сборщик с помощью Collector.of (), хотя сборщик JDK toMap () здесь краткий (+1 здесь ).

Map<String,Integer> newMap = givenMap.
                entrySet().
                stream().collect(Collector.of
               ( ()-> new HashMap<String,Integer>(),
                       (mutableMap,entryItem)-> mutableMap.put(entryItem.getKey(),Integer.parseInt(entryItem.getValue())),
                       (map1,map2)->{ map1.putAll(map2); return map1;}
               ));
Айра
источник
Я начал с этого настраиваемого сборщика в качестве основы и хотел добавить, что, по крайней мере, при использовании parallelStream () вместо stream (), двоичный оператор должен быть переписан во что-то более похожее, map2.entrySet().forEach(entry -> { if (map1.containsKey(entry.getKey())) { map1.get(entry.getKey()).merge(entry.getValue()); } else { map1.put(entry.getKey(),entry.getValue()); } }); return map1иначе значения будут потеряны при уменьшении.
user691154
3

Если вы не возражаете против использования сторонних библиотек, моя библиотека cyclops-реагирует на расширения для всех типов JDK Collection , включая Map . Мы можем просто преобразовать карту напрямую, используя оператор «карта» (по умолчанию карта действует на значения в карте).

   MapX<String,Integer> y = MapX.fromMap(HashMaps.of("hello","1"))
                                .map(Integer::parseInt);

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

  MapX<String,Integer> y = MapX.fromMap(HashMaps.of("hello","1"))
                               .bimap(this::newKey,Integer::parseInt);
Джон МакКлин
источник
0

Декларативное и более простое решение будет:

yourMutableMap.replaceAll ((ключ, val) -> return_value_of_bi_your_function); В северном направлении знать, что вы изменяете свое состояние карты. Так что это может быть не то, что вы хотите.

Приветствия: http://www.deadcoderising.com/2017-02-14-java-8-declarative-ways-of-modifying-a-map-using-compute-merge-and-replace/

Бретон Ф.
источник