Объединить карту массивов с дублирующими ключами
у меня есть две карты массивы.
Map<String, List<String>> map1 = new HashMap<>();
Map<String, List<String>> map2 = new HashMap<>();
Я хочу объединить их в одну новую карту.
Если ключ существует в обеих картах, в этом случае я должен объединить массивы.
например:
map1.put("k1", Arrays.asList("a0", "a1"));
map1.put("k2", Arrays.asList("b0", "b1"));
map2.put("k2", Arrays.asList("z1", "z2"));
// Expected output is
Map 3: {k1=[a0, a1], k2=[b0, b1, z1, z2]}
Я пытался сделать это с потоком
Map<String, List<String>> map3 = Stream.of(map1, map2)
.flatMap(map -> map.entrySet().stream())
.collect(Collectors.toMap(
Map.Entry::getKey,
e -> e.getValue().stream().collect(Collectors.toList())
));
эта работа, если в картах нет одинаковых ключей. В противном случае, я получаю исключение
Exception in thread "main" java.lang.IllegalStateException: Duplicate key k2 (attempted merging values [b0, b1] and [z1, z2])
at java.base/java.util.stream.Collectors.duplicateKeyException(Collectors.java:133)
at java.base/java.util.stream.Collectors.lambda$uniqKeysMapAccumulator(Collectors.java:180)
at java.base/java.util.stream.ReduceOpsReducingSink.accept(ReduceOps.java:169)
at java.base/java.util.HashMap$EntrySpliterator.forEachRemaining(HashMap.java:1751)
at java.base/java.util.stream.ReferencePipeline$Head.forEach(ReferencePipeline.java:658)
at java.base/java.util.stream.ReferencePipeline.accept(ReferencePipeline.java:274)
at java.base/java.util.Spliterators$ArraySpliterator.forEachRemaining(Spliterators.java:948)
at java.base/java.util.stream.AbstractPipeline.copyInto(AbstractPipeline.java:484)
at java.base/java.util.stream.AbstractPipeline.wrapAndCopyInto(AbstractPipeline.java:474)
at java.base/java.util.stream.ReduceOps$ReduceOp.evaluateSequential(ReduceOps.java:913)
at java.base/java.util.stream.AbstractPipeline.evaluate(AbstractPipeline.java:234)
at java.base/java.util.stream.ReferencePipeline.collect(ReferencePipeline.java:578)
at im.djm.Test.main(Test.java:25)
есть ли способ выполнить эту задачу с потоками?
Или мне нужно проходить через карты?
8 ответов
используйте функцию слияния в случае дубликатов ключей:
Map<String, List<String>> map3 = Stream.of(map1, map2)
.flatMap(map -> map.entrySet().stream())
.collect(Collectors.toMap(
Map.Entry::getKey,
e -> new ArrayList<>(e.getValue()),
(left, right) -> {left.addAll(right); return left;}
));
обратите внимание, я изменил e -> e.getValue().stream().collect(Collectors.toList())
to new ArrayList<>(e.getValue())
чтобы гарантировать, что у нас всегда есть изменяемый список, который мы можем добавить в функцию слияния.
может быть. Но вы, скорее всего, получите все правильно, объединив записи вручную, используя итерацию. Я не знаю, придется ли кому-то еще работать над этим кодом, но они, вероятно, будут благодарны за простой для чтения подход.
вы также можете сделать это так:
Map<String, List<String>> map3 = Stream.concat(map1.entrySet().stream(),
map2.entrySet().stream())
.collect(Collectors.groupingBy(Entry::getKey,
Collectors.mapping(Entry::getValue,
Collectors.flatMapping(List::stream,
Collectors.toList()))));
вы должны использовать перегруженный toMap()
версия, которая позволяет объединить дубликаты ключей :
toMap(Function<? super T, ? extends K> keyMapper,
Function<? super T, ? extends U> valueMapper,
BinaryOperator<U> mergeFunction)
вы могли бы написать так :
Map<String, List<String>> map3 = Stream.of(map1, map2)
.flatMap(map -> map.entrySet().stream())
.collect(Collectors.toMap(
Map.Entry::getKey,
e -> new ArrayList<>(e.getValue()),
(e1, e2) -> { e1.addAll(e2); return e1;}
));
использование flatmap дважды
Map<String, List<String>> map1 = new HashMap<>();
Map<String, List<String>> map2 = new HashMap<>();
map1.put("k1", Arrays.asList("a0", "a1"));
map1.put("k2", Arrays.asList("b0", "b1"));
map2.put("k2", Arrays.asList("z1", "z2"));
Map<String, List<String>> map3 = Stream.of(map1, map2)
.flatMap(p -> p.entrySet().stream())
.flatMap(p -> p.getValue().stream().map(q -> new Pair<>(p.getKey(), q)))
.collect(
Collectors.groupingBy(
p -> p.getKey(),
Collectors.mapping(p -> p.getValue(), Collectors.toList())
)
);
это работает как это:
- забирает обе карты
Stream<Map<String,List<String>>>
- FlatMaps записи как
Entry<String, List<String>>
- FlatMaps записи в 1 пару на
Pair<String, String>
- собирает их своим ключом
- принимая значения и собирая их в список
вот пример использования итерации обеих карт. Первая итерация объединяет общие пары ключ / значение из map1 и map2 вместе и добавляет их к результирующей карте или добавляет уникальные пары ключ/значение в map1 к результирующей карте. Вторая итерация захватывает все, что осталось в map2, которое не соответствует map1, и добавляет их в полученную карту.
public static Map<String, ArrayList<String>> joinMaps(Map<String, ArrayList<String>> map1, Map<String, ArrayList<String>> map2)
{
Map<String, ArrayList<String>> mapJoined = new HashMap<>();
//join values from map2 into values of map1 or add unique key/values of map1
for (Map.Entry<String, ArrayList<String>> entry : map1.entrySet()) {
String key = entry.getKey();
ArrayList<String> value = entry.getValue();
if(map2.containsKey(key))
{
value.addAll(map2.get(key));
mapJoined.put(key, value);
}
else
mapJoined.put(key, value);
}
//add the non-duplicates left over in map 2
for (Map.Entry<String, ArrayList<String>> entry : map2.entrySet()) {
if(!mapJoined.containsKey(entry.getKey()))
mapJoined.put(entry.getKey(), entry.getValue());
}
return mapJoined;
}
вы также можете добавить набор в функцию, чтобы отслеживать все ключи, добавленные на первой итерации, а затем, если размер этого набора == size из map2 вы знаете, что карты имеют те же ключи, и нет необходимости повторять вторую карту, map2.
другой способ был бы таким.
вы должны init map3
С большей картой.(здесь map1
). затем используйте loop над другой картой и используйте merge
метод объединения дубликатов ключей.
Map<String, List<String>> map3 = new HashMap<>(map1);
for (Map.Entry<String, List<String>> entry : map2.entrySet()) {
List<String> values = new ArrayList<>(entry.getValue());
map3.merge(entry.getKey(),entry.getValue(),(l1, l2) -> {values.addAll(l1);
return values;
});
}
map2.forEach((key, value) -> {
List<String> values = new ArrayList<>(value);
map3.merge(key,value, (l1, l2) -> {values.addAll(l1);return values;});
});
вот еще один способ объединить карты и списки.
Map<String, List<String>> map3 = Stream.of(map1, map2)
.flatMap(map -> map.entrySet().stream())
.collect(Collectors.toMap(
Map.Entry::getKey,
Map.Entry::getValue,
(e1, e2) -> Stream.concat(e1.stream(), e2.stream()).collect(Collectors.toList())
));
третий аргумент в toMap
метод(e1, e2) -> Stream.concat(e1.stream(), e2.stream()).collect(Collectors.toList())
и .
Эта функция применяется к дубликатам.
если сопоставленные ключи содержат дубликаты (согласно
Object.equals(Object))
, функция сопоставления значений применяется к каждому равному элементу, и результаты объединяются с помощью предоставленной функции слияния.
JavaDoc