Java8流——收集 vs 减少

什么时候使用 collect()reduce()?有没有人有好的、具体的例子来说明什么时候走这条路或那条路是绝对更好的?

Javadoc 提到 Collection ()是一个可变的约简

考虑到它是一个可变的减少,我假设它需要同步(内部) ,这反过来可能对性能有害。据推测,reduce()更容易并行化,代价是必须在 reduce 的每一步之后创建一个新的数据结构以返回。

然而,以上的陈述只是猜测,我希望能有一位专家加入进来。

102575 次浏览

reduce是一个“ 弃牌”操作,它对流中的每个元素应用一个二进制操作符,其中操作符的第一个参数是前一个应用程序的返回值,第二个参数是当前流元素。

collect是一种聚合操作,其中创建了一个“集合”,并将每个元素“添加”到该集合中。然后将流的不同部分中的集合添加到一起。

你链接的文件说明了采取两种不同方法的原因:

如果我们想获取一个字符串流并将它们连接到 单根长弦,我们可以通过普通的简化来达到这个目的:

 String concatenated = strings.reduce("", String::concat)

我们可以得到想要的结果,甚至可以并行工作。 但是,我们可能不会高兴的表现! 这样一个 实现将执行大量的字符串复制,并且运行 时间在字符数上是 O (n ^ 2) 方法是将结果累积到一个 StringBuilder 中, 它是一个可变的容器,用于积累字符串 同样的技术来并行处理可变的约简,就像我们对普通 减少。

所以重点是在这两种情况下并行化是相同的,但是在 reduce的情况下,我们将函数应用到流元素本身。在 collect的例子中,我们将函数应用于一个可变容器。

正常的还原意味着结合两个 永恒不变值,如 int、 double 等,并产生一个新的; 这是一个 永恒不变还原。相比之下,收集方法被设计成 使容器发生突变来积累它应该产生的结果。

为了说明这个问题,让我们假设您希望使用如下简单的约简来实现 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不是线程安全的,在迭代时从它中添加/删除值也不安全,所以当你更新列表或者合并器试图合并列表时,你会得到并发异常或者 ArrayIndexOutOfBoundsException或者任何类型的异常(特别是并行运行时) ,因为你通过累加(添加)整数来改变列表。如果您想使这个线程安全,您需要每次传递一个新的列表,这将损害性能。

相比之下,Collectors.toList()以类似的方式工作。但是,当您将这些值累积到列表中时,它可以保证线程安全。来自 collect方法的文档:

使用 Collector 对此流的元素执行可变的还原操作。如果流是并行的,并且 Collector 是并发的,则为 流是无序的或收集器是无序的,则 同时进行缩减。因此,即使与非线程安全的数据结构(例如 ArrayList)并行执行时,并行减少也不需要额外的同步。

回答你的问题:

什么时候使用 collect()reduce()

如果你有不变的值,比如 intsdoublesStrings,那么正常的还原就可以了。但是,如果您必须将 reduce中的值转换成比如 List(可变数据结构) ,那么您需要使用 collect方法的可变约简。

它们在运行时的潜在内存占用方面是 非常不同的。当 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运行时错误。显而易见的原因是,它试图将通过流的所有数据(事实上,已经存储在数据库中)堆叠到结果集合中,这会导致堆膨胀。

然而,如果你用 reduce()代替 collect()——这将不再是一个问题,因为后者将减少和丢弃所有通过的数据。

在本例中,只需用 reduce代替 collect():

.reduce(0L, (aLong, result) -> aLong, (aLong1, aLong2) -> aLong1);

因为 Java 不是纯 FP (函数式编程)语言,而且由于可能的副作用,不能优化流底部没有使用的数据,所以你甚至不需要关心计算是否依赖于 result

原因很简单:

  • 带有 易变的结果对象的 collect()
  • reduce()是带有 永恒不变结果对象的 是用来工作的

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(Integer.valueOf(6), sum);
}

collect()与可变”示例

例如,如果你想用 collect()手动计算一个和,它不能与 BigDecimal一起工作,但只能与 org.apache.commons.lang.mutableMutableInt一起工作。参见:

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);
}

这是因为 蓄电池 container.add(employee.getSalary().intValue());不应该返回带有结果的新对象,而应该改变类型为 MutableInt的可变 container的状态。

如果你想使用 BigDecimal代替 container,你不能使用 collect()方法,因为 container.add(employee.getSalary());不会改变 container,因为 BigDecimal是不可变的。 (除此之外,BigDecimal::new不能工作,因为 BigDecimal没有空的构造函数)

设流为 a <-b <-c <-d

减少,

你会得到(a # b) # c) # d

你想做的有趣的手术在哪里。

在收藏品中,

你的收藏家会有某种收藏结构 K。

K 消耗一个。 然后 K 消耗 b。 然后 K 消耗 c。 然后 K 消耗 d。

最后,你问 K 最终的结果是什么。

然后 K 给你。

根据 那些文件

当在 groupingBy 或 PartitioningBy 的下游进行多级减少时,reduce ()收集器最为有用。若要对流执行简单的约简,请改用 Stream.reduce (BinaryOperator)。

因此,基本上只有在执行集合操作时才会使用 reducing()。 这是另一个 例子:

 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))));

根据 本教程的降低有时效率较低

Reduce 操作始终返回一个新值。但是,累加器函数每次处理流的元素时也返回一个新值。假设您希望将流的元素减少为更复杂的对象,例如集合。这可能会阻碍应用程序的性能。如果 reduce 操作涉及向集合添加元素,那么每次累加器函数处理一个元素时,都会创建一个包含该元素的新集合,这是低效的。对于您来说,更新一个现有的集合会更有效率。您可以使用 Stream.Collection 方法来完成这项工作,下一节将介绍..。

因此,身份在 reduce 场景中是“重用”的,所以如果可能的话,使用 .reduce会更有效一些。

下面是代码示例

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();

Println (sum) ;

下面是执行结果:

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 ()方法,总是更喜欢收集()方法。使用 Collection ()的性能更好,正如这里所解释的:

Java8教程

* 可变的简化操作(例如 Stream.Collection ())在处理流元素时收集可变结果容器(集合)中的流元素。 与不可变的减少操作(例如 Stream.reduce ())相比,可变的减少操作提供了更好的性能。

这是因为在每个还原步骤中保存结果的集合对于 Collector 是可变的,并且可以在下一步中再次使用。

另一方面,reduce () operation 使用不可变的结果容器,因此需要在降低性能的每个中间步骤 中实例化容器的新实例。*