Answers:
注意: nosid的答案显示了如何使用来添加到现有集合forEachOrdered()
。这是对现有集合进行变异的有用且有效的技术。我的答案解决了为什么您不应该使用A Collector
来突变现有集合的原因。
简短的答案是no,至少在一般情况下不是这样,您不应该使用a Collector
来修改现有集合。
原因是收集器被设计为支持并行性,即使是在不是线程安全的收集器上也是如此。他们这样做的方法是让每个线程根据自己的中间结果集合独立运行。每个线程获取其自己的集合的方式是调用每次Collector.supplier()
返回一个新集合所需的。
然后,再次以线程受限的方式合并这些中间结果的集合,直到只有一个结果集合。这是操作的最终结果collect()
。
来自Balder和Assylias的几个答案建议使用Collectors.toCollection()
然后传递一个返回现有列表而不是新列表的供应商。这违反了供应商的要求,即每次都返回一个新的空集合。
如其答案中的示例所示,这将适用于简单的情况。但是,它将失败,特别是如果流并行运行。(该库的未来版本可能会以某种无法预料的方式更改,即使在连续的情况下也会导致其失败。)
让我们举一个简单的例子:
List<String> destList = new ArrayList<>(Arrays.asList("foo"));
List<String> newList = Arrays.asList("0", "1", "2", "3", "4", "5");
newList.parallelStream()
.collect(Collectors.toCollection(() -> destList));
System.out.println(destList);
当我运行该程序时,通常会收到一个ArrayIndexOutOfBoundsException
。这是因为多个线程正在对ArrayList
一个线程不安全的数据结构进行操作。好的,让我们使其同步:
List<String> destList =
Collections.synchronizedList(new ArrayList<>(Arrays.asList("foo")));
这将不会再因异常而失败。但是,而不是预期的结果:
[foo, 0, 1, 2, 3]
它给出了如下奇怪的结果:
[foo, 2, 3, foo, 2, 3, 1, 0, foo, 2, 3, foo, 2, 3, 1, 0, foo, 2, 3, foo, 2, 3, 1, 0, foo, 2, 3, foo, 2, 3, 1, 0]
这是我上面描述的线程限制的累积/合并操作的结果。在并行流的情况下,每个线程都会调用供应商以获取自己的集合以进行中间累积。如果传递的供应商返回相同的集合,则每个线程会将其结果附加到该集合。由于线程之间没有顺序,结果将以任意顺序附加。
然后,当这些中间集合被合并时,这基本上将列表与其自身合并。使用合并列表List.addAll()
,表示如果在操作过程中修改了源集合,则结果是不确定的。在这种情况下,请ArrayList.addAll()
执行阵列复制操作,因此最终会自我复制,这大概是我期望的。(请注意,其他List实现可能具有完全不同的行为。)无论如何,这解释了奇怪的结果和目标中重复的元素。
您可能会说:“我将确保按顺序运行流”并继续编写这样的代码
stream.collect(Collectors.toCollection(() -> existingList))
无论如何。我建议不要这样做。如果可以控制流,那么可以保证它不会并行运行。我希望会出现一种编程风格,即流而不是集合被传递。如果有人将流交给您,并且您使用此代码,则如果流碰巧是并行的,它将失败。更糟糕的是,有人可能会递给您一个顺序流,并且此代码将在一段时间内正常工作,通过所有测试等。然后,在任意时间后,系统中其他地方的代码可能会更改为使用并行流,这将导致您的代码打破。
确定,然后确保sequential()
在使用此代码之前记得记得在任何流上调用:
stream.sequential().collect(Collectors.toCollection(() -> existingList))
当然,您会记得每次都这样做,对吗?:-)假设您愿意。然后,性能团队会想知道为什么他们所有精心设计的并行实现都没有提供任何加速。然后他们再次将其追溯到您的代码,这迫使整个流按顺序运行。
不要这样
toCollection
每次都说服我不要这样做时,该方法的provider参数应返回一个新的空集合。我真的想打破核心Java类的javadoc契约。
forEachOrdered
。副作用包括向现有集合中添加元素,无论它是否已经具有元素。如果你想有一个流的元素放入一个新的收集,使用collect(Collectors.toList())
或toSet()
或toCollection()
。
据我所知,到目前为止,所有其他答案都使用收集器将元素添加到现有流中。但是,有一个较短的解决方案,它适用于顺序流和并行流。您可以简单地将forEachOrdered方法与方法参考结合使用。
List<String> source = ...;
List<Integer> target = ...;
source.stream()
.map(String::length)
.forEachOrdered(target::add);
唯一的限制是,源和目标是不同的列表,因为只要处理了流,就不允许对流的源进行更改。
请注意,此解决方案适用于顺序流和并行流。但是,它不能从并发中受益。传递给forEachOrdered的方法引用将始终按顺序执行。
forEachOrdered
代替您使用forEach
?
forEachOrdered
适用于顺序流和并行流。相反,forEach
对于并行流,可能会同时执行传递的函数对象。在这种情况下,必须正确同步功能对象,例如通过使用Vector<Integer>
。
target::add
。无论从哪个线程调用该方法,都不会发生数据争用。我希望您知道这一点。
简短的答案是“否”(或应该为“否”)。编辑:是的,这是可能的(请参阅下面的assylias答案),但请继续阅读。EDIT2:但是出于另一个原因您仍然不应该这样做,请参阅Stuart Marks的答案!
更长的答案:
Java 8中这些构造的目的是向语言引入一些函数式编程的概念。在函数式编程中,通常不修改数据结构,而是通过转换(例如映射,过滤器,折叠/缩小等)在旧数据结构的基础上创建新数据结构。
如果必须修改旧列表,只需将映射的项目收集到新列表中:
final List<Integer> newList = list.stream()
.filter(n -> n % 2 == 0)
.collect(Collectors.toList());
然后做list.addAll(newList)
-再次:如果你真的必须。
(或构建一个新的列表拼接旧与新的一个,并将其分配回list
变,这是一个有点更FP比灵addAll
)
关于API:即使API允许这样做(同样,请参见assylias的答案),您也应至少避免这样做,至少通常是这样。最好不要对抗范式(FP)并尝试学习它而不是对抗它(即使Java通常不是FP语言),并且只有在绝对需要时才诉诸“较脏”的策略。
答案很长:(即,如果您包括按照建议实际查找和阅读FP简介/书籍的工作)
要找出为什么修改现有列表通常是一个坏主意,并且导致代码的可维护性较差-除非您要修改局部变量并且算法简短和/或琐碎,否则这超出了代码可维护性问题的范围-对函数式编程(有成百上千种)进行了很好的介绍,并开始阅读。“预览”解释将类似于:在数学上更合理,更容易推理出不修改数据(在程序的大多数部分中),并导致更高的水平和更少的技术性(一旦您的大脑也更人性化)脱离了程序逻辑的老式命令式定义)。
Erik Allik已经给出了很好的理由,为什么您很可能不想将流的元素收集到现有List中。
无论如何,如果确实需要此功能,则可以使用以下单线。
但是正如Stuart Marks在他的回答中所解释的那样,如果流可能是并行流,则绝对不要这样做-使用后果自负...
list.stream().collect(Collectors.toCollection(() -> myExistingList));
您只需要参考原始列表即可Collectors.toList()
返回该列表。
这是一个演示:
import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;
public class Reference {
public static void main(String[] args) {
List<Integer> list = Arrays.asList(1, 2, 3, 4, 5);
System.out.println(list);
// Just collect even numbers and start referring the new list as the original one.
list = list.stream()
.filter(n -> n % 2 == 0)
.collect(Collectors.toList());
System.out.println(list);
}
}
这是将新创建的元素仅一行添加到原始列表中的方法。
List<Integer> list = ...;
// add even numbers from the list to the list again.
list.addAll(list.stream()
.filter(n -> n % 2 == 0)
.collect(Collectors.toList())
);
这就是该函数式编程范例所提供的。
我将旧列表和新列表连接为流,并将结果保存到目标列表。并行工作也很好。
我将使用Stuart Marks给出的可接受答案的示例:
List<String> destList = Arrays.asList("foo");
List<String> newList = Arrays.asList("0", "1", "2", "3", "4", "5");
destList = Stream.concat(destList.stream(), newList.stream()).parallel()
.collect(Collectors.toList());
System.out.println(destList);
//output: [foo, 0, 1, 2, 3, 4, 5]
希望能帮助到你。
Collection
”