【问题标题】:How to remove multiple elements from Set/Map AND knowing which ones were removed?如何从 Set/Map 中删除多个元素并知道哪些元素已被删除?
【发布时间】:2019-10-28 04:14:41
【问题描述】:

我有一个方法必须从一些(可能很大)Map<K,V> from 中删除(小)Set<K> keysToRemove 中列出的任何元素。但是removeAll() 不行,因为我需要返回所有实际删除的键,因为地图可能包含也可能不包含需要删除的键。

老派代码直截了当:

public Set<K> removeEntries(Map<K, V> from) {
    Set<K> fromKeys = from.keySet();
    Set<K> removedKeys = new HashSet<>();
    for (K keyToRemove : keysToRemove) {
        if (fromKeys.contains(keyToRemove)) {
            fromKeys.remove(keyToRemove);
            removedKeys.add(keyToRemove);
        }
    }
    return removedKeys;
}

同样,使用流编写:

Set<K> fromKeys = from.keySet();
return keysToRemove.stream()
        .filter(fromKeys::contains)
        .map(k -> {
            fromKeys.remove(k);
            return k;
        })
        .collect(Collectors.toSet());

我觉得这更简洁一些,但我也觉得 lambda 太笨重了。

有什么建议可以以不那么笨拙的方式实现相同的结果吗?

【问题讨论】:

  • 如何收集所有可以删除的密钥,然后在该过滤集上调用removeAll()?或者在fromKeys::remove上“过滤”怎么样?
  • 我相信并从这里的答案推断,主要来自任何更改的改进是使用 if (fromKeys.remove(keyToRemove)) { removedKeys.add(keyToRemove); } 而不是在 if (fromKeys.contains(keyToRemove)) { fromKeys.remove(keyToRemove); removedKeys.add(keyToRemove); } 中同时使用包含和删除

标签: java lambda java-stream


【解决方案1】:

“老派代码”应该是

public Set<K> removeEntries(Map<K, ?> from) {
    Set<K> fromKeys = from.keySet(), removedKeys = new HashSet<>(keysToRemove);
    removedKeys.retainAll(fromKeys);
    fromKeys.removeAll(removedKeys);
    return removedKeys;
}

既然你说keysToRemove 相当小,那么复制开销可能并不重要。否则,使用循环,但不要进行两次哈希查找:

public Set<K> removeEntries(Map<K, ?> from) {
    Set<K> fromKeys = from.keySet();
    Set<K> removedKeys = new HashSet<>();
    for(K keyToRemove : keysToRemove)
        if(fromKeys.remove(keyToRemove)) removedKeys.add(keyToRemove);
    return removedKeys;
}

您可以将相同的逻辑表达为流

public Set<K> removeEntries(Map<K, ?> from) {
    return keysToRemove.stream()
        .filter(from.keySet()::remove)
        .collect(Collectors.toSet());
}

但由于这是一个有状态的过滤器,因此强烈建议不要这样做。一个更干净的变体是

public Set<K> removeEntries(Map<K, ?> from) {
    Set<K> result = keysToRemove.stream()
        .filter(from.keySet()::contains)
        .collect(Collectors.toSet());
    from.keySet().removeAll(result);
    return result;
}

如果你想最大化“流式”的使用,你可以用from.keySet().removeIf(result::contains)替换from.keySet().removeAll(result);,因为它正在迭代更大的地图,或者result.forEach(from.keySet()::remove),它不会有这个缺点,但仍然没有比removeAll 更具可读性。

总而言之,“老派代码”要好得多。

【讨论】:

  • @Naman 这就是我发布的第二个变体,适用于迭代很重要的情况。但是,retainAll/removeAll 组合将遍历 OP 指定的相当小的集合。
  • @Naman 正在触及实现细节,但我认为它的工作方式类似于AbstractSet.removeAll(…),即使没有继承该方法的权利:“这个实现确定哪个是这个集合中的较小者和指定集合,通过在每个集合上调用 size 方法。 …[等]”。对partitioningBy 使用有状态谓词与filter 一样不鼓励,但使用后者,您将收集另一组实际上不需要的元素……
  • @cs95 好吧,是的,对于大多数 SO 答案,我编写了一些测试代码,要么从头开始,要么使用问题的代码作为起点,如果有的话。根据上下文,它可能在 Netbeans、Eclipse 或命令行中。当涉及到与编译器相关的行为时,我也有批处理文件来使用不同的 JDK 编译和运行相同的源代码。
  • @Naman 我的最后一句话写得很匆忙。我想说的是,partitioningBy 在需要时做更多的工作,而只需要两组中的一组。除此之外,它就像filter 方法。
  • @Marco13 我经常这样做,尤其是对于包含示例的问题,但并非每个答案都会从示例中受益。此外,并不是我所有的测试代码都是一个最小的例子。有时,它会针对其他测试进行编辑,而不是一次在代码中进行所有测试,因此在发布之前需要进行大量清理。
【解决方案2】:

更简洁的解决方案,但在filter 调用中仍然存在不需要的副作用

Set<K> removedKeys =
    keysToRemove.stream()
                .filter(fromKeys::remove)
                .collect(Collectors.toSet());

如果set 包含指定的元素,Set.remove 已经返回true

最后,我可能会坚持使用“老式代码”。

【讨论】:

  • 正是我的想法 ;) - 只是感觉有点 hacky,因为我们正在“过滤”一个实际上代表副作用的方法。
【解决方案3】:

我不会为此使用 Streams。我会利用retainAll

public Set<K> removeEntries(Map<K, V> from) {
    Set<K> matchingKeys = new HashSet<>(from.keySet());
    matchingKeys.retainAll(keysToRemove);

    from.keySet().removeAll(matchingKeys);

    return matchingKeys;
}

【讨论】:

  • 这指向正确的方向,但是您正在复制“可能大”from 映射的键集,而您可以复制“小”keysToRemove,因为 a 和 b 的交集是与 b 和 a 相同。此外,matchingKeys 可能小于keysToRemove,因此removeAll(matchingKeys) 更可取。
  • @Holger 我明白你的意思,但 Set 只是复制引用,这对我来说似乎是良性的,除非地图的大小真的很大。不过,您对 removeAll(matchingKeys) 是正确的。已更新。
  • 这不仅仅是复制引用,而是散列。而且由于 OP 说明了预期的大小并且交换两者是微不足道的,我会这样做。其实I did.
【解决方案4】:

你可以使用流和removeAll

Set<K> fromKeys = from.keySet();
Set<K> removedKeys = keysToRemove.stream()
    .filter(fromKeys::contains)
    .collect(Collectors.toSet());
fromKeys.removeAll(removedKeys);
return removedKeys;

【讨论】:

    【解决方案5】:

    你可以用这个:

    Set<K> removedKeys = keysToRemove.stream()
            .filter(from::containsKey)
            .collect(Collectors.toSet());
    removedKeys.forEach(from::remove);
    

    这类似于 Oleksandr 的回答,但避免了副作用。但是,如果您正在寻找性能,我会坚持这个答案。

    或者,您可以使用Stream.peek() 进行删除,但要小心其他副作用(请参阅 cmets)。所以我不建议这样做。

    Set<K> removedKeys = keysToRemove.stream()
            .filter(from::containsKey)
            .peek(from::remove)
            .collect(Collectors.toSet());
    

    【讨论】:

    【解决方案6】:

    要向方法添加另一种变体,还可以对键进行分区并将所需的Set 返回为:

    public Set<K> removeEntries(Map<K, ?> from) {
        Map<Boolean, Set<K>> partitioned = keysToRemove.stream()
                .collect(Collectors.partitioningBy(k -> from.keySet().remove(k),
                        Collectors.toSet()));
        return partitioned.get(Boolean.TRUE);
    }
    

    【讨论】:

    • 还可以选择使用不属于地图键集的键。 (以防万一)
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2015-07-18
    • 1970-01-01
    • 2021-03-13
    • 1970-01-01
    • 2011-06-24
    • 1970-01-01
    相关资源
    最近更新 更多