Java8: HashMap <X, Y> в HashMap <X, Z>, використовуючи Stream / Map-Reduce / Collector


209

Я знаю, як "перетворити" просту 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наведеному вище прикладі, я хотів би викликати будь-який метод (або конструктор).

Відповіді:


372
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 видає непосильну помилку - він каже, що Функція не застосовується для Функції, що може заплутати: "Метод transformValues ​​(Map <K, V1>, Function <? Super V1 , V2>) у типі Maps не застосовується для аргументів (Map <Foo, Bar>, Function <Bar, Baz>) "
mskfisher

Якщо ви повинні пройти a java.util.Function, у вас є два варіанти. 1. Уникайте проблеми, використовуючи лямбда, щоб дозволити висновку типу Java. 2. Використовуйте посилання на метод, наприклад, javaFunction :: застосувати, щоб створити нову лямбда, з якої можна визначити умовиводи.
Джо

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 (), хоча toMap () колектор JDK тут є стислим (+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;}
               ));

Я почав із цього користувальницького колектора в якості основи і хотів додати, що принаймні при використанні paralStream () замість потоку () binaryOperator слід переписати на щось більш схоже, 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або значення будуть втрачені при зменшенні.
користувач691154

3

Якщо ви не заперечуєте над використанням сторонніх бібліотек, у моїй бібліотеці, що реагує на циклопи, є розширення для всіх типів колекції JDK , включаючи Map . Ми можемо просто перетворити карту безпосередньо за допомогою оператора «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);

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.