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 ответов:
Map<String, String> x; Map<String, Integer> y = x.entrySet().stream() .collect(Collectors.toMap( e -> e.getKey(), e -> Integer.parseInt(e.getValue()) ));
Это не так хорошо, как код списка. Вы не можете построить новые
Map.Entry
s в вызове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;} ));