Comment produire une carte avec des valeurs distinctes d'une carte (et utiliser la bonne touche en utilisant BinaryOperator)?


13

J'ai une carte Map<K, V>et mon objectif est de supprimer les valeurs dupliquées et de reproduire la même structure Map<K, V>. Dans le cas où la valeur dupliquée est trouvée, il faut sélectionner une clé ( k) parmi les deux clés ( k1et k1) qui contiennent ces valeurs, pour cette raison, supposons que le BinaryOperator<K>don kde k1et k2est disponible.

Exemple d'entrée et de sortie:

// Input
Map<Integer, String> map = new HashMap<>();
map.put(1, "apple");
map.put(5, "apple");
map.put(4, "orange");
map.put(3, "apple");
map.put(2, "orange");

// Output: {5=apple, 4=orange} // the key is the largest possible

Ma tentative d'utilisation Stream::collect(Supplier, BiConsumer, BiConsumer)est un peu maladroite et contient des opérations mutables telles que Map::putet Map::removeque j'aimerais éviter:

// // the key is the largest integer possible (following the example above)
final BinaryOperator<K> reducingKeysBinaryOperator = (k1, k2) -> k1 > k2 ? k1 : k2;

Map<K, V> distinctValuesMap = map.entrySet().stream().collect(
    HashMap::new,                                                              // A new map to return (supplier)
    (map, entry) -> {                                                          // Accumulator
        final K key = entry.getKey();
        final V value = entry.getValue();
        final Entry<K, V> editedEntry = Optional.of(map)                       // New edited Value
            .filter(HashMap::isEmpty)
            .map(m -> new SimpleEntry<>(key, value))                           // If a first entry, use it
            .orElseGet(() -> map.entrySet()                                    // otherwise check for a duplicate
                    .stream() 
                    .filter(e -> value.equals(e.getValue()))
                    .findFirst()
                    .map(e -> new SimpleEntry<>(                               // .. if found, replace
                            reducingKeysBinaryOperator.apply(e.getKey(), key), 
                            map.remove(e.getKey())))
                    .orElse(new SimpleEntry<>(key, value)));                   // .. or else leave
        map.put(editedEntry.getKey(), editedEntry.getValue());                 // put it to the map
    },
    (m1, m2) -> {}                                                             // Combiner
);

Existe-t-il une solution utilisant une combinaison appropriée de l' Collectorsintérieur d'un Stream::collectappel (par exemple sans opérations mutables)?


2
Quelles sont vos mesures pour " mieux " ou " meilleur "? Doit-on le faire par Streams?
— Turing85

Si la même valeur est associée à 2 clés, comment choisissez-vous quelle clé est conservée?
— Michael

Quels sont les résultats attendus dans votre cas?
— YCF_L

1
@ Turing85: Comme je l'ai dit. Le meilleur ou le meilleur serait sans utilisation explicite de méthodes de carte mutables telles que Map::putou Map::removedans le Collector.
— Nikolas

1
Cela vaut la peine d'y jeter un coup d'œil BiMap. Peut-être un doublon de Supprimer les valeurs en double de HashMap en Java
— Naman

Réponses:


12

Vous pouvez utiliser Collectors.toMap

private Map<Integer, String> deduplicateValues(Map<Integer, String> map) {
    Map<String, Integer> inverse = map.entrySet().stream().collect(toMap(
            Map.Entry::getValue,
            Map.Entry::getKey,
            Math::max) // take the highest key on duplicate values
    );

    return inverse.entrySet().stream().collect(toMap(Map.Entry::getValue, Map.Entry::getKey));
}

9

Essayez ceci: de manière simple, inversez la clé et la valeur, puis utilisez le toMap()collecteur avec la fonction de fusion.

map.entrySet().stream()
        .map(entry -> new AbstractMap.SimpleEntry<>(entry.getValue(), entry.getKey()))
        .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue, reducingKeysBinaryOperator));

Map<K, V> output = map.entrySet().stream()
        .collect(Collectors.toMap(Map.Entry::getValue, Map.Entry::getKey, reducingKeysBinaryOperator))
        .entrySet().stream()
        .collect(Collectors.toMap(Map.Entry::getValue, Map.Entry::getKey));

2
Je ne vois pas ce que l' mapopération intermédiaire achète. Vous semblez échanger des clés et des valeurs, c'est clair, mais quel est le point, vous pourriez faire cela à l'étape de collecte tout de même?
— GPI

3
@GPI et Michael, c'est parce qu'il doit fusionner les clés, donc inverser les paires fusionnera les clés. Ce qui manque alors, c'est la deuxième inversion.
— Jean-Baptiste Yunès

2
@HadiJ Non! L'inversion était correcte! mais une seconde était nécessaire pour revenir. La fusion est utilisée pour fusionner les clés, mais la fusion n'est possible que pour les valeurs ...
— Jean-Baptiste Yunès

@ Jean-BaptisteYunès Je comprends la nécessité de fusionner, mais pourquoi je ne comprends pas tout de suite pourquoi vous codez swap(); collect(key, value, binOp);au lieu de collect(value, key, binOp). Peut-être que je dois essayer ça en vrai pour de vrai?
— GPI

2
A pris la liberté d'utiliser la variable locale introduite dans la question dans le code partagé par vous. Revenez au cas où cela contredirait votre intention pendant que vous répondiez.
— Naman

4

Je trouve la solution non-streams plus expressive:

BinaryOperator<K> reducingKeysBinaryOperator = (k1, k2) -> k1 > k2 ? k1 : k2;

Map<V, K> reverse = new LinkedHashMap<>(map.size());
map.forEach((k, v) -> reverse.merge(v, k, reducingKeysBinaryOperator));

Map<K, V> result = new LinkedHashMap<>(reverse.size());
reverse.forEach((v, k) -> result.put(k, v));

Ceci utilise Map.mergeavec votre bi-fonction réductrice et utilise LinkedHashMappour conserver l'ordre des entrées d'origine.


2
Oui, j'ai conclu cette solution (similaire). Cependant, je recherche l' approche java-stream , car c'est la manière la plus déclarative. Avoir mon +1
— Nikolas

1

J'ai trouvé un moyen d'utiliser uniquement Collectorssans avoir besoin de collecter et de traiter à nouveau la carte retournée. L'idée est:

  1. Groupe le Map<K, V>à Map<V, List<K>.

    Map<K, V> distinctValuesMap = this.stream.collect(
        Collectors.collectingAndThen(
            Collectors.groupingBy(Entry::getValue),
            groupingDownstream 
        )
    );

    {pomme = [1, 5, 3], orange = [4, 2]}

  2. Réduisez les nouvelles clés ( List<K>) à l' Kutilisation BinaryOperator<K>.

    Function<Entry<V, List<Entry<K, V>>>, K> keyMapFunction = e -> e.getValue().stream()
        .map(Entry::getKey)
        .collect(Collectors.collectingAndThen(
            Collectors.reducing(reducingKeysBinaryOperator),
            Optional::get
        )
    );

    {pomme = 5, orange = 4}

  3. Inversez à Map<V, K>nouveau le retour à la Map<K, V>structure - ce qui est sûr car les clés et les valeurs sont garanties comme distinctes.

    Function<Map<V, List<Entry<K,V>>>, Map<K, V>> groupingDownstream = m -> m.entrySet()
        .stream()
        .collect(Collectors.toMap(
            keyMapFunction,
            Entry::getKey
        )
    );

    {5 = pomme, 4 = orange}

Le code final:

final BinaryOperator<K> reducingKeysBinaryOperator = ...

final Map<K, V> distinctValuesMap = map.entrySet().stream().collect(
        Collectors.collectingAndThen(
            Collectors.groupingBy(Entry::getValue),
            m -> m.entrySet().stream().collect(
                Collectors.toMap(
                    e -> e.getValue().stream().map(Entry::getKey).collect(
                        Collectors.collectingAndThen(
                            Collectors.reducing(reducingKeysBinaryOperator),
                            Optional::get
                        )
                    ),
                    Entry::getKey
                )
            )
        )
    );

1

Une autre approche pour obtenir le résultat souhaité avec "Stream and Collectors.groupingBy".

    map = map.entrySet().stream()
    .collect(Collectors.groupingBy(
            Entry::getValue,
            Collectors.maxBy(Comparator.comparing(Entry::getKey))
            )
    )
    .entrySet().stream()
    .collect(Collectors.toMap(
            k -> {
                return k.getValue().get().getKey();
            }, 
            Entry::getKey));
En utilisant notre site, vous reconnaissez avoir lu et compris notre politique liée aux cookies et notre politique de confidentialité.
Licensed under cc by-sa 3.0 with attribution required.