Java8: HashMap to HashMap using Stream / Map-Reduce / Collector


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

9 145

9 ответов:

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().

Вот некоторые варианты ответаСотириоса Делиманолиса , который был довольно хорош для начала (+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() дает ключ и значение как отдельные параметры, тогда как поток имеет только одно значение - и вы должны выбрать, Использовать ли ключ или запись карты в качестве этого значения. С другой стороны, здесь не хватает богатой, струящейся доброты другого подходы. :- )

Общее решение, подобное этому

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

Должен ли он быть абсолютно функциональным и беглым на 100%? Если нет, то как насчет этого, который примерно такой же короткий, как и получается:

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

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

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

Map<String, Integer> output = EntryStream.of(input).mapValues(Integer::valueOf).toMap();

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

Maps.transformValues(originalMap, val -> ...)

Если вы не возражаете использовать сторонние библиотеки, мой cyclops-react lib имеет расширения для всех типов JDK Collection, включая Map. Мы можем просто преобразовать карту непосредственно с помощью оператора 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);

Альтернативой, которая всегда существует для целей обучения, является создание пользовательского коллектора через коллектор.хотя tomap () JDK collector здесь лаконичен (+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;}
               ));

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

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

Ура : http://www.deadcoderising.com/2017-02-14-java-8-declarative-ways-of-modifying-a-map-using-compute-merge-and-replace/