您何时会使用collect()
vs reduce()
?有没有人有一个好的,具体的例子,说明什么时候选择这种方式肯定会更好?
Javadoc mentions that collect() is a mutable reduction
鉴于它是一个可变的减少,我认为它需要同步(内部),这反过来可能对性能有害。据推测,reduce()
更容易并行化,代价是必须在reduce中的每个步骤之后创建一个新的数据结构。
然而,上述陈述是猜测,我希望有一位专家在这里发声。
答案 0 :(得分:95)
reduce
是一个“fold”操作,它将二元运算符应用于流中的每个元素,其中运算符的第一个参数是前一个应用程序的返回值,第二个参数是当前流元素。
collection
是一个聚合操作,其中创建“集合”并将每个元素“添加”到该集合。然后将流的不同部分中的集合加在一起。
document you linked给出了采用两种不同方法的原因:
如果我们想要获取字符串流并将它们连接成一个 单长字符串,我们可以通过普通缩减实现这一点:
String concatenated = strings.reduce("", String::concat)
我们会得到理想的结果,甚至可以并行工作。 但是,我们可能对性能不满意!这样的 实现会进行大量的字符串复制和运行 时间将是字符数O(n ^ 2)。性能更高 方法是将结果累积到StringBuilder中, 这是一个用于累积字符串的可变容器。我们可以使用 与普通人一样,将可变减少并行化的技术相同 还原
所以重点是两种情况下的并行化是相同的,但在reduce
情况下,我们将函数应用于流元素本身。在collect
的情况下,我们将函数应用于可变容器。
答案 1 :(得分:30)
原因很简单:
reduce()
只能与可变结果对象一起使用。public class Employee {
private Integer salary;
public Employee(String aSalary){
this.salary = new Integer(aSalary);
}
public Integer getSalary(){
return this.salary;
}
}
@Test
public void testReduceWithImmutable(){
List<Employee> list = new LinkedList<>();
list.add(new Employee("1"));
list.add(new Employee("2"));
list.add(new Employee("3"));
Integer sum = list
.stream()
.map(Employee::getSalary)
.reduce(0, (Integer a, Integer b) -> Integer.sum(a, b));
assertEquals(new Integer(6), sum);
}
旨在与不可变结果对象一起使用。collect()
使用不可变的&#34;示例collect()
BigDecimal
可变&#34;示例 E.g。如果您想使用MutableInt
手动计算总和,则它无法与org.apache.commons.lang.mutable
一起使用,但仅适用于来自public class Employee {
private MutableInt salary;
public Employee(String aSalary){
this.salary = new MutableInt(aSalary);
}
public MutableInt getSalary(){
return this.salary;
}
}
@Test
public void testCollectWithMutable(){
List<Employee> list = new LinkedList<>();
list.add(new Employee("1"));
list.add(new Employee("2"));
MutableInt sum = list.stream().collect(
MutableInt::new,
(MutableInt container, Employee employee) ->
container.add(employee.getSalary().intValue())
,
MutableInt::add);
assertEquals(new MutableInt(3), sum);
}
的{{1}}。参见:
container.add(employee.getSalary().intValue());
这是有效的,因为accumulator container
不应该返回带有结果的新对象,而是要更改MutableInt
类型的可变BigDecimal
的状态。 / p>
如果您想使用container
代替collect()
,则无法使用container.add(employee.getSalary());
方法,因为container
不会更改BigDecimal
,因为{ {1}}它是不可变的。
(除此之外BigDecimal::new
不起作用,因为BigDecimal
没有空构造函数)
答案 2 :(得分:20)
正常缩减意味着将两个不可变值组合起来,例如int,double等,并生成一个新值;这是一个不可变的减少。相反,collect方法旨在改变容器以累积它应该产生的结果。
为了说明问题,我们假设您希望使用下面的简单缩减来实现Collectors.toList()
List<Integer> numbers = stream.reduce( new ArrayList<Integer>(),
(List<Integer> l, Integer e) -> {
l.add(e);
return l;
},
(List<Integer> l1, List<Integer> l2) -> {
l1.addAll(l2); return l1; });
这相当于Collectors.toList()
。但是,在这种情况下,您会改变List<Integer>
。我们知道ArrayList
不是线程安全的,在迭代时添加/删除它也不安全,所以你会得到并发异常或arrayIndexOutBound异常或任何类型的异常(特别是当并行运行时)您更新列表或组合器尝试合并列表,因为您通过累积(添加)整数来改变列表。如果你想使这个线程安全,你需要每次都传递一个新列表,这会影响性能。
相比之下,Collectors.toList()
以类似的方式运作。但是,当您将值累积到列表中时,它可以保证线程安全。来自collect
方法的文档:
使用收集器对此流的元素执行可变减少操作。如果流是并行的,并且收集器是并发的,则任何一个 流是无序的或收集器是无序的,然后是 将同时进行减少。 并行执行时,可以实例化,填充和合并多个中间结果,以便保持可变数据结构的隔离。 因此,即使与非线程安全数据并行执行也是如此结构(例如ArrayList),并行缩减不需要额外的同步。 link
所以回答你的问题:
您何时会使用
collect()
vsreduce()
?
如果您有ints
,doubles
,Strings
等不可变值,那么正常缩减就可以了。但是,如果您必须reduce
将您的值设为List
(可变数据结构),那么您需要使用collect
方法进行可变缩减。
答案 3 :(得分:7)
令流为&lt; -b&lt; -c&lt; -d
减少,
你将拥有((a#b)#c)#d
其中#是您想要做的有趣操作。
在收藏中,
你的收藏家将拥有某种收集结构K.
K消耗了一个。 然后K消耗b。 然后K消耗c。 K然后消耗d。最后,你问K最终结果是什么。
然后K给你了。答案 4 :(得分:2)
它们在运行时潜在的内存占用非常不同。当collect()
收集并将所有数据放入集合时,reduce()
会明确要求您指定如何减少通过流的数据。
例如,如果您想从文件中读取一些数据,处理它并将其放入某个数据库,您最终可能会得到类似于此的java流代码:
streamDataFromFile(file)
.map(data -> processData(data))
.map(result -> database.save(result))
.collect(Collectors.toList());
在这种情况下,我们使用collect()
强制java流式传输数据并将结果保存到数据库中。如果没有collect()
,则永远不会读取数据,也不会存储数据。
如果文件大小足够大或堆大小足够低,此代码会愉快地生成java.lang.OutOfMemoryError: Java heap space
运行时错误。显而易见的原因是,它试图将通过流(实际上已经存储在数据库中)的所有数据堆叠到生成的集合中,这会使堆崩溃。
但是,如果您将collect()
替换为reduce()
- 它将不再是问题,因为后者会减少并丢弃所有通过它的数据。
在演示的示例中,只需将collect()
替换为reduce
:
.reduce(0L, (aLong, result) -> aLong, (aLong1, aLong2) -> aLong1);
您甚至不需要关心使计算取决于result
,因为Java不是纯FP(函数式编程)语言,并且不能优化未在底部使用的数据。因为可能的副作用而流。
答案 5 :(得分:1)
以下是代码示例
List<Integer> list = Arrays.asList(1,2,3,4,5,6,7);
int sum = list.stream().reduce((x,y) -> {
System.out.println(String.format("x=%d,y=%d",x,y));
return (x + y);
}).get();
的System.out.println(总和);
这是执行结果:
x=1,y=2
x=3,y=3
x=6,y=4
x=10,y=5
x=15,y=6
x=21,y=7
28
Reduce函数句柄有两个参数,第一个参数是前一个返回值int的流,第二个参数是当前的 计算流中的值,它将第一个值和当前值相加作为下一个计算中的第一个值。
答案 6 :(得分:0)
根据the docs
reduce()收集器在用于多级缩减时,在groupingBy或partitioningBy的下游最有用。要对流执行简单缩减,请改用Stream.reduce(BinaryOperator)。
所以基本上你只有在强制收集时才使用reducing()
。
这是另一个example:
For example, given a stream of Person, to calculate the longest last name
of residents in each city:
Comparator<String> byLength = Comparator.comparing(String::length);
Map<String, String> longestLastNameByCity
= personList.stream().collect(groupingBy(Person::getCity,
reducing("", Person::getLastName, BinaryOperator.maxBy(byLength))));
根据this tutorial,有时效率降低
reduce操作始终返回一个新值。但是,累加器函数每次处理流的元素时也会返回一个新值。假设您要将流的元素减少为更复杂的对象,例如集合。这可能会妨碍您的应用程序的性能。如果reduce操作涉及向集合添加元素,那么每次accumulator函数处理元素时,它都会创建一个包含元素的新集合,这是低效的。相反,更新现有集合会更有效。您可以使用Stream.collect方法执行此操作,下一节将介绍...
因此,在减少方案中“重新使用”身份,如果可能的话,使用.reduce
的效率会略高一些。
答案 7 :(得分:0)
有一个很好的理由总是偏爱collect()而不是reduce()方法。使用collect()的性能要好得多,如下所述:
*一个可变的约简操作(例如Stream.collect())在处理流元素时将其收集在一个可变结果容器(collection)中。 与不变的约简操作(例如Stream.reduce())相比,可变的约简操作提供了更高的性能。
这是由于以下事实:保存在每个归约步骤中的结果的集合对于收集器来说是可变的,并且可以在下一步中再次使用。
另一方面,Stream.reduce()操作使用不可变的结果容器,因此需要在每个还原的中间步骤实例化容器的新实例,从而降低性能。*