如何从一个映射中生成具有不同值的映射(并使用BinaryOperator使用正确的键)?

12

我有一个名为Map<K, V>的地图,我的目标是删除重复的值并输出完全相同的结构Map<K, V>。如果找到重复的值,则必须从保存这些值的两个键(k1k2)中选择一个键(k),因此,假设可以使用BinaryOperator<K>来从k1k2中提供k

输入和输出示例:

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

我尝试使用 Stream::collect(Supplier, BiConsumer, BiConsumer),但实现起来有些笨拙,而且包含了像Map::putMap::remove这样的可变操作,我想要避免这种情况:

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

是否可以在一个Stream::collect调用中使用适当组合的Collectors解决问题(例如,无需可变操作)?


2
你对于“更好”或“最佳”的度量标准是什么?必须通过Stream完成吗? - Turing85
如果同一个值与两个键相关联,您如何选择保留哪个键? - Michael
在您的情况下,预期输出是什么? - Youcef LAIDANI
1
@Turing85: 正如我所说的那样。更好或最好的方法是在Collector内部没有显式使用可变Map方法,如Map::putMap::remove - Nikolas Charalambidis
1
值得一看BiMap。可能是Remove duplicate values from HashMap in Java的副本。 - Naman
显示剩余3条评论
5个回答

11

您可以使用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));
}

8
试试这个:简单的方法是反转键和值,然后使用 toMap() 收集器与合并函数。
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
我不明白中间的map操作有什么作用。你似乎是在交换键和值,这一点很清楚,但是这样做有什么意义呢?你可以在收集步骤中同样地完成这个操作。 - GPI
3
@GPI和Michael,这是因为他需要合并键,所以反转键值对将合并键。缺失的是第二次反转。 - Jean-Baptiste Yunès
2
@HadiJ 不!反转是正确的!但是还需要第二个来回。合并用于合并键,但只有值才能进行合并... - Jean-Baptiste Yunès
@Jean-BaptisteYunès 我理解合并的必要性,但我不明白为什么你编写 swap(); collect(key, value, binOp); 而不是 collect(value, key, binOp)。也许我需要在 jshell 中尝试一下? - GPI
2
在您提出的代码中,我使用了问题中引入的本地变量。如果这样做与您回答问题时的意图有冲突,请回复我。 - Naman
简单明了!运行得很好:) 是否可能在一个“Stream”中实现完全相同的结果,而无需使用处理-收集-处理管道? - Nikolas Charalambidis

4

我认为非流式解决方案更具表现力:

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

这里使用了 Map.merge 以及你的双重函数来减少代码量,并且使用 LinkedHashMap 来保留原始条目顺序。


1
我发现一种只使用Collectors而无需再次收集和处理返回的Map的方法。思路如下:
  1. Group the Map<K, V> to Map<V, List<K>.

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

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

  2. Reduce the new keys (List<K>) to K using 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
        )
    );
    

    {apple=5, orange=4}

  3. Inverse the Map<V, K> back to Map<K, V> structure again - which is safe since both keys and values are guaranteed as distinct.

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

    {5=apple, 4=orange}

最终代码:

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

使用“Stream和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));

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接