Java8: HashMap <X, Y> в HashMap <X, Z> с использованием Stream/Map-Reduce/Collector
Я знаю, как "преобразовать" простую Java List
из Y
→ Z
, то есть:
List<String> x;
List<Integer> y = x.stream()
.map(s -> Integer.parseInt(s))
.collect(Collectors.toList());
Теперь я хотел бы сделать в основном то же самое с Map, т.е.:
INPUT:
{
"key1" -> "41", // "41" and "42"
"key2" -> "42 // are Strings
}
OUTPUT:
{
"key1" -> 41, // 41 and 42
"key2" -> 42 // are Integers
}
Решение не должно ограничиваться String
→ Integer
. Как и в примере List
выше, я хотел бы вызвать любой метод (или конструктор).
Ответы
Ответ 1
Map<String, String> x;
Map<String, Integer> y =
x.entrySet().stream()
.collect(Collectors.toMap(
e -> e.getKey(),
e -> Integer.parseInt(e.getValue())
));
Это не так хорошо, как код списка. Вы не можете построить новый Map.Entry
в вызове map()
, чтобы работа была смешана с вызовом collect()
.
Ответ 2
Вот несколько вариантов ответа Sotirios Delimanolis, который был очень хорош для начала (+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()
дает ключ и значение как отдельные параметры, тогда как поток имеет только одно значение - и вам нужно выбрать, использовать ли ключ или запись карты как это значение. На минусовой стороне этого не хватает богатой, поточной доброты других подходов.: -)
Ответ 3
Такое общее решение
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));
Ответ 4
Нужно ли быть на 100% функциональным и свободно? Если нет, как насчет этого, что примерно так же коротко, как и получается:
Map<String, Integer> output = new HashMap<>();
input.forEach((k, v) -> output.put(k, Integer.valueOf(v));
(если вы можете жить с позором и виной в сочетании потоков с побочными эффектами)
Ответ 5
My StreamEx библиотека, которая улучшает стандартный поток API, предоставляет EntryStream
, который лучше подходит для преобразования карт:
Map<String, Integer> output = EntryStream.of(input).mapValues(Integer::valueOf).toMap();
Ответ 6
Функция Guava Maps.transformValues
- это то, что вы ищете, и оно прекрасно работает с лямбда-выражениями:
Maps.transformValues(originalMap, val -> ...)
Ответ 7
Альтернатива, которая всегда существует для целей обучения, заключается в том, чтобы создать свой пользовательский коллекционер через 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;}
));
Ответ 8
Если вы не возражаете против использования сторонних библиотек, cyclops-react lib имеет расширения для всех JDK Collection, включая 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);
Ответ 9
Декларативное и более простое решение:
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/