Como produzir mapa com valores distintos de um mapa (e usar a tecla certa usando BinaryOperator)?

13

Eu tenho um mapa Map<K, V>e meu objetivo é remover os valores duplicados e gerar a mesma estrutura Map<K, V>novamente. Caso o valor duplicado for encontrado, deve ser selecionada uma chave ( k) das duas teclas ( k1e k1) que detêm esses valores, por isso, assumir a BinaryOperator<K>dar ka partir k1e k2está disponível.

Exemplo de entrada e saída:

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

Minha tentativa de usar Stream::collect(Supplier, BiConsumer, BiConsumer)é um pouco desajeitada e contém operações mutáveis ​​como Map::pute Map::removeque eu gostaria de 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 uma solução usando uma combinação apropriada de Collectorsdentro de uma Stream::collectchamada (por exemplo, sem operações mutáveis)?

Nikolas
fonte
2
Qual é a sua métrica para " melhor " ou " melhor "? Não deve ser feito através de Streams?
Turing85
Se o mesmo valor estiver associado a 2 chaves, como você escolhe qual chave será mantida?
Michael
Quais são os resultados esperados no seu caso?
YCF_L
11
@ Turing85: Como eu disse. O melhor ou o melhor seria sem o uso explícito de métodos de mapas mutáveis, como Map::putou Map::removedentro do Collector.
Nikolas
11
Vale a pena dar uma olhada BiMap. Possivelmente uma duplicata de Remover valores duplicados do HashMap em Java
Naman 6/01

Respostas:

12

Você pode 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
fonte
9

Tente o seguinte: A maneira simples é inversa à chave e ao valor e use o toMap()coletor com a função de mesclagem.

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
fonte
2
Não consigo ver o que a mapoperação intermediária compra. Você parece trocar chaves e valores, isso é claro, mas qual é o objetivo? Você poderia fazer isso na etapa de coleta da mesma forma?
GPI
3
@GPI e Michael, isso é porque ele precisa mesclar as chaves, então inverter os pares mesclará as chaves. O que está faltando é a segunda inversão então.
Jean-Baptiste Yunès
2
@HadiJ Não! A inversão estava correta! mas era necessário um segundo para voltar. A mesclagem é usada para mesclar as chaves, mas a mesclagem só é possível para valores ...
Jean-Baptiste Yunès
@ Jean-BaptisteYunès Entendo a necessidade de mesclar, mas por que não entendo imediatamente é por que você codifica em swap(); collect(key, value, binOp);vez de collect(value, key, binOp). Talvez eu precise tentar isso em um jshell de verdade?
GPI
2
Você teve a liberdade de usar a variável local introduzida na pergunta no código compartilhado por você. Reverta se houver conflito de intenção enquanto você estava respondendo.
Naman 6/01
4

Acho a solução sem fluxos mais expressiva:

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

Isso é usado Map.mergecom a sua bi-função redutora e é usado LinkedHashMappara preservar a ordem das entradas originais.

Federico Peralta Schaffner
fonte
2
Sim, concluí esta solução (semelhante). No entanto, estou procurando a abordagem java-stream , pois é a maneira mais declarativa. Tenho o meu +1
Nikolas
1

Encontrei uma maneira de usar apenas Collectorssem a necessidade de coletar e processar novamente o mapa retornado. A ideia é:

  1. Agrupe o Map<K, V>para Map<V, List<K>.

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

    {maçã = [1, 5, 3], laranja = [4, 2]}

  2. Reduza as novas 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
        )
    );

    {maçã = 5, laranja = 4}

  3. Inverta Map<V, K>novamente a Map<K, V>estrutura - o que é seguro, pois as chaves e os valores são garantidos como distintos.

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

    {5 = maçã, 4 = laranja}

O 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
fonte
1

Outra maneira de obter o resultado desejado com "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
fonte