如何从“设置/地图”中删除多个元素并知道删除了哪些元素?

Gho*_*ica 29 java lambda java-stream

我有一种方法必须Set<K> keysToRemove从一些(可能大)中删除(小)中列出的任何元素Map<K,V> from。但是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;
}
Run Code Online (Sandbox Code Playgroud)

使用流写的相同:

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

我发现这更加简洁,但是我也发现lambda太笨拙了。

任何建议如何以较少笨拙的方式实现相同结果?

Hol*_*ger 24

“老式代码”应该是

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

由于您说的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;
}
Run Code Online (Sandbox Code Playgroud)

您可以将与流一样的逻辑表示为

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

但是,由于这是一个有状态的过滤器,因此不建议使用。更干净的变体是

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;
}
Run Code Online (Sandbox Code Playgroud)

而且,如果您想最大限度地利用“流式”用法,则可以替换from.keySet().removeAll(result);from.keySet().removeIf(result::contains),价格昂贵,因为它在较大的地图上进行迭代;或者替换为result.forEach(from.keySet()::remove),它虽然没有缺点,但可读性却不高比removeAll

总而言之,“老式代码”比这要好得多。

  • @ cs95很好,对于大多数SO答案,我都会从头开始编写一些测试代码,或者以问题的代码为起点(如果有)。根据上下文,它可能在Netbeans,Eclipse或命令行中。当涉及到依赖于编译器的行为时,我还有批处理文件,可使用不同的JDK编译和运行相同的源代码。 (3认同)
  • @Naman涉及实现细节,但我认为它的工作方式类似于[`AbstractSet.removeAll(…)`](https://docs.oracle.com/en/java/javase/11/docs/api/java.base /java/util/AbstractSet.html#removeAll(java.util.Collection)),即使没有继承该方法:“ *此实现通过调用size方法来确定哪个是该集合和指定集合中的较小者。每。…[等等]*”。不鼓励将有状态谓词用于“ partitioningBy”和“过滤器”,但是对于后者,您将收集另一组实际上不需要的元素… (2认同)
  • @ Marco13我经常这样做,特别是对于包含示例的问题,但并非每个答案都会从示例中受益。此外,并非我的所有测试代码都是一个最小的示例。有时,它会针对其他测试进行编辑,而一次没有在代码中包含所有测试,因此在发布之前需要进行大量清理。 (2认同)

Ole*_*hov 13

更简洁的解决方案,但在通话中仍会产生有害的副作用filter

Set<K> removedKeys =
    keysToRemove.stream()
                .filter(fromKeys::remove)
                .collect(Collectors.toSet());
Run Code Online (Sandbox Code Playgroud)

Set.removetrue如果set包含指定的元素,则已经返回。

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

  • 完全是我的想法;)-感觉有点,因为我们正在“过滤”实际上代表副作用的方法。 (4认同)

VGR*_*VGR 5

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

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

    from.keySet().removeAll(matchingKeys);

    return matchingKeys;
}
Run Code Online (Sandbox Code Playgroud)

  • 这指向正确的方向,但是您正在从地图的键集中复制“可能很大”的键,而可以复制“小的” keysToRemove的键,因为a和b的交集与b和a的交集相同。此外,“ matchingKeys”可能比“ keysToRemove”更小,因此“ removeAll(matchingKeys)”是可取的。 (3认同)
  • 这不仅是复制引用,而且是散列。而且由于OP指出了预期的大小,并且将两者交换是微不足道的,所以我会这样做。实际上,[我确实](/sf/answers/3960884511/)。 (3认同)