Слияние двух карт с Java 8 Stream API
у меня есть два (или более) Map<String, Integer>
объекты. Я хотел бы объединить их с Java 8 Stream API таким образом, чтобы значения для общих ключей были максимальными из значений.
@Test
public void test14() throws Exception {
Map<String, Integer> m1 = ImmutableMap.of("a", 2, "b", 3);
Map<String, Integer> m2 = ImmutableMap.of("a", 3, "c", 4);
List<Map<String, Integer>> list = newArrayList(m1, m2);
Map<String, Integer> mx = list.stream()... // TODO
Map<String, Integer> expected = ImmutableMap.of("a", 3, "b", 3, "c", 4);
assertEquals(expected, mx);
}
как я могу сделать этот метод тестирования зеленым?
Я играл с collect
и Collectors
некоторое время без всякого успеха.
(ImmutableMap
и newArrayList
из Google Guava.)
7 ответов:
@Test public void test14() throws Exception { Map<String, Integer> m1 = ImmutableMap.of("a", 2, "b", 3); Map<String, Integer> m2 = ImmutableMap.of("a", 3, "c", 4); Map<String, Integer> mx = Stream.of(m1, m2) .map(Map::entrySet) // converts each map into an entry set .flatMap(Collection::stream) // converts each set into an entry stream, then // "concatenates" it in place of the original set .collect( Collectors.toMap( // collects into a map Map.Entry::getKey, // where each entry is based Map.Entry::getValue, // on the entries in the stream Integer::max // such that if a value already exist for // a given key, the max of the old // and new value is taken ) ) ; /* Use the following if you want to create the map with parallel streams Map<String, Integer> mx = Stream.of(m1, m2) .parallel() .map(Map::entrySet) // converts each map into an entry set .flatMap(Collection::stream) // converts each set into an entry stream, then // "concatenates" it in place of the original set .collect( Collectors.toConcurrentMap( // collects into a map Map.Entry::getKey, // where each entry is based Map.Entry::getValue, // on the entries in the stream Integer::max // such that if a value already exist for // a given key, the max of the old // and new value is taken ) ) ; */ Map<String, Integer> expected = ImmutableMap.of("a", 3, "b", 3, "c", 4); assertEquals(expected, mx); }
mx = list.stream().collect(HashMap::new, (a, b) -> b.forEach((k, v) -> a.merge(k, v, Integer::max)), Map::putAll);
Это охватывает общий случай для любого списка размеров и должно работать с любыми типами, просто замените
Integer::max
и/илиHashMap::new
по желанию.Если вам все равно, какое значение выходит в слиянии, есть гораздо более чистое решение:
mx = list.stream().collect(HashMap::new, Map::putAll, Map::putAll);
и как общие методы:
public static <K, V> Map<K, V> mergeMaps(Stream<? extends Map<K, V>> stream) { return stream.collect(HashMap::new, Map::putAll, Map::putAll); } public static <K, V, M extends Map<K, V>> M mergeMaps(Stream<? extends Map<K, V>> stream, BinaryOperator<V> mergeFunction, Supplier<M> mapSupplier) { return stream.collect(mapSupplier, (a, b) -> b.forEach((k, v) -> a.merge(k, v, mergeFunction)), Map::putAll); }
я добавил свой вклад в протонной библиотека, который содержит служебные методы для API поток. Вот как вы могли бы достичь того, что вы хотите:
Map<String, Integer> mx = MapStream.ofMaps(m1, m2).mergeKeys(Integer::max).collect();
в принципе
mergeKeys
соберет пары ключ-значение в новой карте (при условии, что функция слияния является необязательной, вы получитеMap<String, List<Integer>>
в противном случае) и вспомнитьstream()
наentrySet()
, чтобы получить новыйMapStream
. Тогда используйтеcollect()
чтобы получить результирующую карту.
С помощью StreamEx вы можете сделать:
StreamEx.of(m1, m2) .flatMapToEntry(x -> x) .grouping(IntCollector.max())