【问题标题】:Add objects from stream to two different lists simultaneously同时将流中的对象添加到两个不同的列表
【发布时间】:2016-04-21 09:01:37
【问题描述】:

如何将一个流中的对象同时添加到两个不同的列表中

目前我在做

body.getSurroundings().parallelStream()
                .filter(o -> o.getClass().equals(ResourcePoint.class))
                .map(o -> (ResourcePoint)o)
                .filter(o -> !resourceMemory.contains(o))
                .forEach(resourceMemory::add);

将我的流中的对象添加到链接列表“resourceMemory”中,但我也想同时将相同的对象添加到另一个列表中,但我找不到它的语法。是否有可能或者我需要为每个列表准备两份此代码?

【问题讨论】:

  • 您可以使用peek() 或自定义收集器。
  • 您是指o instanceof ResourcePoint 还是o.getClass() == ResourcePoint.class?决定一个,但不要使用o.getClass().equals(ResourcePoint.class),这会混淆实际意图。此外,在使用并行流时,您的代码会以多种方式被破坏。请仔细阅读docs.oracle.com/javase/8/docs/api/?java/util/stream/…
  • 这个问题的严格答案是.forEach(o -> { resourceMemory.add(o); myOtherList.add(o); })。但请注意霍尔格的评论。为了修复此代码,您需要详细说明您真正想要实现的目标。

标签: java list java-8 java-stream


【解决方案1】:

在尝试扩展代码之前,您应该首先了解几个基本错误。

首先,forEach 不保证元素处理的特定顺序,因此添加到List 可能是错误的工具,即使对于顺序流也是如此,但是,与 parallel 流添加到像 LinkedList 这样不是线程安全的集合中,因为该操作将同时执行

但是即使resourceMemory 是一个线程安全的集合,您的代码仍然被破坏,因为您的filter 条件和终端操作之间存在干扰。 .filter(o -> !resourceMemory.contains(o)) 查询您在终端操作中修改的同一个列表,不难理解即使使用线程安全集合,它也会停止:

两个或多个线程可能会处理过滤器并发现该元素不包含在列表中,然后它们都会添加该元素,这与您不重复的明显意图相矛盾。

您可以求助于forEachOrdered,它将按顺序和非并发地执行操作:

body.getSurroundings().parallelStream()
    .filter(o -> o instanceof ResourcePoint)
    .map(o -> (ResourcePoint)o)
    .forEachOrdered(o -> {// not recommended, just for explanation
        if(!resourceMemory.contains(o))
            resourceMemory.add(o);
    });

这会起作用,很明显你可以如何在该操作中添加到另一个列表,但它与推荐的编码风格相去甚远。此外,此终端操作与所有处理线程同步这一事实将破坏并行处理的任何潜在好处,特别是当此流管道最昂贵的操作是在 LinkedList 上调用 contains 时(必须) 发生在单线程中。

将流元素收集到列表中的正确方法是通过,顾名思义,collect:

List<ResourcePoint> resourceMemory
    =body.getSurroundings().parallelStream()
        .filter(o -> o instanceof ResourcePoint)
        .map(o -> (ResourcePoint)o)
        .distinct()                    // no duplicates
        .collect(Collectors.toList()); // collect into a list

这不会返回LinkedList,但您应该仔细考虑是否真的需要LinkedList。在 99% 的情况下,你没有。如果您真的需要LinkedList,可以将Collectors.toList() 替换为Collectors.toCollection(LinkedList::new)

现在,如果您确实必须添加到在您的控件之外创建的现有列表中,该列表可能已经包含元素,您应该考虑上面提到的事实,即您必须确保对非线程安全列表的单线程访问无论如何,所以从并行流中执行它根本没有任何好处。在大多数情况下,让流独立于该列表工作并随后在单个线程步骤中添加结果会更有效:

Set<ResourcePoint> newElements=
    body.getSurroundings().parallelStream()
        .filter(o -> o instanceof ResourcePoint)
        .map(o -> (ResourcePoint)o)
        .collect(Collectors.toCollection(LinkedHashSet::new));
newElements.removeAll(resourceMemory);
resourceMemory.addAll(newElements);

在这里,我们收集到一个LinkedHashSet,这意味着维护遭遇顺序并整理出新元素中的重复项,然后在新元素上使用removeAll删除目标列表中的现有元素(这里我们受益于临时集合的哈希集性质),最后,新元素被添加到目标列表中,正如解释的那样,对于非线程安全的目标集合,无论如何都必须单线程发生。

使用此解决方案很容易将newElements 添加到另一个目标集合,比编写自定义收集器在流处理期间生成两个列表要容易得多。但请注意,上面写的流操作太便宜了,无法从并行处理中获得任何好处。您将需要大量元素来补偿初始多线程开销。甚至有可能没有任何数字可以带来回报。

【讨论】:

    【解决方案2】:

    代替

    .forEach(resourceMemory::add)
    

    你可以调用

    .forEach(o -> {
       resourceMemory.add(o);
       otherResource.add(o);
     })
    

    或将添加操作放在单独的方法中,以便提供方法参考

    .forEach(this::add)
    
    void add(ResourcePoint p) {
       resourceMemory.add(o);
       otherResource.add(o);
    }
    

    但请记住,当您使用并行流时,每次运行的插入顺序可能不同。

    【讨论】:

    • @Holger 您的评论可能出现在错误的位置。我不认为它指的是上面的答案。
    • @Markus,不,霍尔格的评论是正确的,我编辑了我的答案,被破坏的部分被删除,如果代码被破坏,修复/删除它:)
    • 应该强调的是,传递给forEach 的操作不仅以任意顺序调用,而且同时调用,因此,这仅适用于线程安全集合。跨度>
    猜你喜欢
    • 2013-06-23
    • 2019-04-02
    • 2015-08-09
    • 1970-01-01
    • 2022-01-21
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多