将对象从流同时添加到两个不同的列表

时间:2016-04-21 09:01:38

标签: java list java-8 java-stream

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

目前我在做

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

将我的流中的对象添加到链表“resourceMemory”中,但我也希望同时将相同的对象添加到另一个列表,但我找不到它的语法。是否可以或者我是否需要为每个列表提供此代码的两个副本?

2 个答案:

答案 0 :(得分:3)

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

首先,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);
    });

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

将流元素收集到列表中的正确方法是,顾名思义,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添加到另一个目标集合很容易,比在流处理期间编写自定义收集器以生成两个列表要容易得多。但请注意,上面写的流操作太过于难以承担并行处理的任何好处。您需要非常多的元素来补偿初始的多线程开销。甚至可能没有任何数字能够得到回报。

答案 1 :(得分:1)

而不是

.forEach(resourceMemory::add)

你可以调用

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

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

.forEach(this::add)

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

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