¿Cómo producir un mapa con valores distintos de un mapa (y usar la tecla correcta usando BinaryOperator)?

13

Tengo un mapa Map<K, V>y mi objetivo es eliminar los valores duplicados y generar la misma estructura Map<K, V>nuevamente. En caso de que se encuentre el valor duplicado, debe seleccionarse una clave ( k) de las dos claves ( k1y k1) que contienen estos valores, por esta razón, asumir la BinaryOperator<K>entrega kde k1y k2está disponible.

Ejemplo de entrada y salida:

// 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

Mi intento de usar Stream::collect(Supplier, BiConsumer, BiConsumer)es un poco torpe y contiene operaciones mutables como Map::puty Map::removeque me gustaría evitar:

// // 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 una solución que use una combinación adecuada de Collectorsuna Stream::collectllamada (por ejemplo, sin operaciones mutables)?

Nikolas
fuente
2
¿Cuál es su métrica para " mejor " o " mejor "? ¿Debe hacerse a través de Streams?
Turing85
Si el mismo valor está asociado con 2 claves, ¿cómo elige qué clave se retiene?
Michael
¿Cuáles son los resultados esperados en su caso?
YCF_L
1
@ Turing85: Como dije. Lo mejor o lo mejor sería sin el uso explícito de métodos de mapas mutables como Map::puto Map::removedentro de Collector.
Nikolas
1
Vale la pena echarle un vistazo BiMap. Posiblemente un duplicado de Eliminar valores duplicados de HashMap en Java
Naman

Respuestas:

12

Puedes usar 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
fuente
9

Pruebe esto: la manera simple es invertir la clave y el valor y luego usar el toMap()recopilador con la función de combinación.

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
fuente
2
No veo qué mapcompra la operación intermedia . Pareces intercambiar claves y valores, eso está claro, pero ¿cuál es el punto, podrías hacer eso en el paso de recopilación de la misma manera?
GPI
3
@GPI y Michael, esto se debe a que tiene que fusionar las claves, por lo que invertir los pares fusionará las claves. Lo que falta es la segunda inversión entonces.
Jean-Baptiste Yunès
2
@HadiJ ¡No! ¡La inversión fue correcta! pero era necesario un segundo para volver. Fusionar se usa para fusionar las claves, pero la fusión solo es posible para valores ...
Jean-Baptiste Yunès
@ Jean-BaptisteYunès Entiendo la necesidad de fusionarme, pero la razón por la que no entiendo de inmediato es por qué codificas en swap(); collect(key, value, binOp);lugar de hacerlo collect(value, key, binOp). ¿Quizás necesito probar esto en un jshell de verdad?
GPI
2
Tomó la libertad de usar la variable local introducida en la pregunta en el código compartido por usted. Revierta en caso de que entre en conflicto con la intención mientras estaba respondiendo.
Naman
4

Encuentro que la solución no streams es más expresiva:

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

Esto se usa Map.mergecon su bi-función reductora y se usa LinkedHashMappara preservar el orden de las entradas originales.

Federico Peralta Schaffner
fuente
2
Sí, he concluido esta solución (similar). Sin embargo, estoy buscando el enfoque java-stream , ya que es la forma más declarativa. Tengo mi +1
Nikolas
1

Encontré una forma de usar solo Collectorssin necesidad de recopilar y procesar nuevamente el Mapa devuelto. La idea es:

  1. Agrupa el Map<K, V>a Map<V, List<K>.

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

    {manzana = [1, 5, 3], naranja = [4, 2]}

  2. Reduzca las nuevas teclas ( List<K>) para Kusar 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
        )
    );

    {manzana = 5, naranja = 4}

  3. Invierta la parte Map<V, K>posterior a la Map<K, V>estructura nuevamente, lo cual es seguro ya que tanto las claves como los valores están garantizados como distintos.

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

    {5 = manzana, 4 = naranja}

El código 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
fuente
1

Otra aproximación para obtener el resultado deseado con "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
fuente