web-dev-qa-db-fra.com

Fusion de deux mappes <String, Integer> avec Java 8 Stream API

J'ai deux (ou plus) Map<String, Integer> objets. Je souhaite les fusionner avec Java 8 Stream API de manière à ce que les valeurs des clés communes soient égales au maximum des valeurs.

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

Comment puis-je rendre cette méthode de test verte?

J'ai joué avec collect et Collectors pendant un moment sans aucun succès.

(ImmutableMap et newArrayList proviennent de Google Guava.)

63
user3528157
@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);
}
114
srborlongan
Map<String, Integer> mx = new HashMap<>(m1);
m2.forEach((k, v) -> mx.merge(k, v, Integer::max));
64
Stuart Marks
mx = list.stream().collect(HashMap::new,
        (a, b) -> b.forEach((k, v) -> a.merge(k, v, Integer::max)),
        Map::putAll);

Cela couvre le cas général de toute liste de tailles et devrait fonctionner avec tous les types, il suffit d’échanger le Integer::max et/ou HashMap::new comme voulu.

Si vous ne vous souciez pas de la valeur qui ressort d'une fusion, il existe une solution beaucoup plus propre:

mx = list.stream().collect(HashMap::new, Map::putAll, Map::putAll);

Et comme méthodes génériques:

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);
}
15
Sean Van Gorder

J'ai créé une représentation visuelle de ce que @srborlongan a fait pour tous les intéressés.

Diagram displaying maps convert to stream of entries

11

J'ai ajouté ma contribution à la bibliothèque de packs de protons qui contient des méthodes utilitaires pour l'API Stream. Voici comment vous pouvez réaliser ce que vous voulez:

Map<String, Integer> mx = MapStream.ofMaps(m1, m2).mergeKeys(Integer::max).collect();

Fondamentalement, mergeKeys collectera les paires clé-valeur dans une nouvelle carte (à condition qu'une fonction de fusion soit facultative, vous obtiendrez un Map<String, List<Integer>> Sinon) et rappellera stream() sur la entrySet() pour obtenir un nouveau MapStream. Ensuite, utilisez collect() pour obtenir la carte obtenue.

2
Alexis C.

En utilisant StreamEx vous pouvez faire:

StreamEx.of(m1, m2)
    .flatMapToEntry(x -> x)
    .grouping(IntCollector.max())
1
Graeme Moss