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)?

Nikolas
la source
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));
}
MikeFHay
la source
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));
Hadi J
la source
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.

Federico Peralta Schaffner
la source
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
                )
            )
        )
    );
Nikolas
la source
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));
vishesh chandra
la source