【问题标题】:Java Stream Map Filter, Remove and CollectJava Stream Map 过滤、移除和收集
【发布时间】:2021-05-08 22:15:03
【问题描述】:

我有一个包含 1000 个项目的 Map,我想从这个 Map 的前 500 个项目中生成一个 List,并删除从 Map 收集的项目。

换句话说,我想过滤、收集到List 并从Map 中删除项目。

我正在尝试这样的事情:

final int i = 0;
int max = 5;
Map<String, Object> map = new HashMap<>();
map.put("ads", "123");
map.put("qwe", "123");
map.put("cvb", "123");
map.put("asd", "123");
map.put("iop", "123");
map.put("jkl", "123");
map.put("yui", "123");

List list = map.entrySet().stream().filter(y -> i++ < max).collect(Collectors.toList());

预期输出:

具有 2 个值的映射

包含 5 个值的列表

【问题讨论】:

  • 不清楚你想做什么,并且发布的代码一开始就没有编译。请提供您想要做的示例输入和预期输出,例如只有 10 个元素。
  • 请注意,在执行.collect.forEach 之类的终端操作后,您将无法再使用Stream。
  • @ÓscarLópez 感谢您的反馈,我对问题进行了一些更改并添加了输入和输出。
  • 另外,请注意entrySet() 不保证其元素的顺序。它们不一定是您添加的前五个,除非您使用的是LinkedHashMap。但总的来说,从地图中获取“前 n 个元素”的想法没有意义,如果顺序很重要,也许您应该使用不同的数据结构。
  • 最后意味着i一旦分配就不能更改。所以你不能这样做filter(y -&gt; i++ &lt; max). 即使你没有声明i final,局部变量在lambdas中也必须是有效的。

标签: java collections java-stream


【解决方案1】:

Stream API 旨在创建一个新集合,而不是更改现有集合,从该集合创建一个流。

在这种情况下最好使用迭代器功能。像这样:

final int MAX = 500;

Map<String, String> map = new HashMap<>();
map.put("Item1", "Item 1 value");
map.put("Item2", "Item 2 value");
// ...
map.put("Item1000", "Item 1000 value");

List<String> list = new ArrayList<>();


var iterator = map.entrySet().iterator();
for (int i = 0; i< MAX; i++) {
  if (iterator.hasNext()) {
    var item = iterator.next();
    list.add(item.getValue());
    iterator.remove();
  }
}

【讨论】:

  • LinkedHashMap 配合使用可以很好地获取第一个MAX 项目。请参阅有关 Óscar López 的这个问题的评论。
  • 使用迭代器保证删除添加到列表中的完全相同的条目。不管是 LinkedHashMap 还是 HashMap。
  • 是的,您对 IteratorLinkedHashMap 保证从地图中删除前 500 个插入的项目是正确的。
  • 无需遍历entrySet() 即可在每个条目上调用getValue()。你可以简单地使用values()var iterator = map.values().iterator(); for(int i = 0; i &lt; MAX &amp;&amp; iterator.hasNext(); i++) { list.add(iterator.next()); iterator.remove(); }
  • 由同一个映射支持的两个迭代器的底层机制。因此 values().iterator() 相对于 entrySet().iterator 没有性能优势。但是 entrySet().iterator() 也允许你访问入口键。
【解决方案2】:

正如在其他答案和 cmets 中指出的那样,您不能在流操作期间更改地图。所以这需要两个步骤。

var list = map.keySet().stream().limit(500).toList();
map.keySet().removeAll(list);

我在这里使用了 Java 16 API,但以前的版本是细微的变化。

但是请注意,鉴于未定义地图的顺序,这是一件毫无意义的事情。如果您使用带有某些谓词的过滤器,那将更有意义。尽管在这种情况下,更易读的替代方案可能是:

var list = map.keySet().stream().filter(predicate).toList();
map.keySet().removeIf(predicate);

【讨论】:

  • var list = new ArrayList&lt;String&gt;(); map.keySet().removeIf(predicate.and(list::add));
【解决方案3】:

使用单流实现此目的的最简单方法是使用Stream#limitCollectors#collectingAndThen 并在第二个下游@987654325 内执行removeAll @

这是可能的,因为第一个Collectors.toList 将摆脱地图的迭代,然后第二个操作可以删除而不会出现异常

List<Map.Entry<String, Object>> list = map.entrySet()
        .stream()
        .limit(max)
        .collect(Collectors.collectingAndThen(
                Collectors.toList(),
                l -> {
                    map.entrySet().removeAll(l);
                    return l;
                }
        ));

System.out.println("map = " + map); // map = {qwe=123, cvb=123}
System.out.println("list = " + list); // list = [ads=123, asd=123, jkl=123, iop=123, yui=123]

【讨论】:

  • 我将其标记为重复的原因之一是.entrySet().stream().limit(max) 不能保证“第一个max 元素”。想给你指点Oscar's comment
  • 你可以用旧的removeAll(l)代替removeIf(l::contains)
  • removeAll 作为收集器整理器而不是之后作为单独的语句有什么优势吗?在这里使用 lambda 块似乎不必要地冗长(并且存在与具有副作用的 lambda 操作相关的风险)。
  • 这里的想法是单排。短跑选手你会想到什么样的副作用?
猜你喜欢
  • 1970-01-01
  • 2015-07-26
  • 1970-01-01
  • 1970-01-01
  • 2021-03-16
  • 1970-01-01
  • 1970-01-01
  • 2016-11-10
  • 1970-01-01
相关资源
最近更新 更多