Java 8 Streams – 收集vs减less

你什么时候使用collect() vs reduce() ? 有没有人有一个很好的具体的例子,什么时候最好走一条路?

Javadoc提到collect()是一个可变的减less 。

鉴于这是一个可变的减less,我认为它需要同步(内部),这反过来可能是有害的performance。 大概reduce()是更容易并行化的代价是必须创build一个新的数据结构,以减less每一步之后的返回。

以上的陈述是猜测,但我很乐意专家在这里钟声。

首先,返回值是不同的:

 <R,A> R collect(Collector<? super T,A,R> collector) T reduce(T identity, BinaryOperator<T> accumulator) 

所以collect返回任何Rreduce回报TStream的types。

reduce是一个“ 折叠 ”操作,它将二元运算符应用到stream中的每个元素,其中第一个参数是前一个应用程序的返回值,第二个参数是当前stream元素。

collection是一个聚集操作,其中创build“集合”,并将每个元素“添加”到该集合中。 然后把stream中不同部分的集合加在一起。

你链接的文件给出了两种不同方法的理由:

如果我们想要把一串string连接起来,并把它们连接成一个单一的长string,我们可以用普通的还原来实现:

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

我们会得到期望的结果,甚至可以并行工作。 但是,我们可能不会对表演感到高兴! 这样的实现会进行大量的string复制,运行时间将会是字符数量的O(n ^ 2)。 更高效的方法是将结果累积到一个StringBuilder中,这是一个用于累加string的可变容器。 我们可以使用相同的技术来平行化可变的缩减,就像我们用普通的缩减一样。

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

原因很简单:

  • collect() 只能使用可变结果对象。
  • reduce()devise为不可变的结果对象一起工作

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()与可变”的例子

例如,如果您想使用collect()手动计算总和,则不能使用BigDecimal而只能使用来自org.apache.commons.lang.mutable MutableInt 。 看到:

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

这是因为accumulator container.add(employee.getSalary().intValue()); 不应该返回一个带有结果的新对象,而是改变MutableInttypes的可变containerMutableInt

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

正常的减less是为了结合两个不可变的值,如int,double等,并产生一个新的; 这是一个不可改变的减less。 相比之下,收集方法的目的是改变一个容器,以积累它应该产生的结果。

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

相反, Collectors.toList()以类似的方式工作。 但是,当您将值累加到列表中时,它会保证线程安全。 从collect方法的文档:

使用收集器对此stream的元素执行可变减less操作。 如果stream是并行的,并且收集器是并发的,并且stream是无序的或者收集器是无序的,那么将执行并发的减less。 当并行执行时,可以实例化,填充和合并多个中间结果,以保持可变数据结构的隔离。 因此,即使与非线程安全的数据结构(如ArrayList)并行执行,并行还原也不需要额外的同步。 链接

所以要回答你的问题:

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

如果你有不可改变的值如intsdoublesStrings那么正常的还原就可以了。 然而,如果你不得不把你的值reduce到一个List (可变数据结构),那么你需要使用collect方法使用可变的减less。

它们在运行时潜在的内存占用差异很大。 collect()收集所有数据并将其放入集合,而reduce()明确要求您指定如何减less通过stream的数据。

例如,如果你想从一个文件中读取一些数据,处理它,并把它放到一个数据库中,你可能会得到类似于这样的javastream代码:

 streamDataFromFile(file) .map(data -> processData(data)) .map(result -> database.save(result)) .collect(Collectors.toList()); 

在这种情况下,我们使用collect()来强制Java通过数据stream,并将结果保存到数据库中。 如果没有collect() ,数据永远不会被读取,也不会被存储。

如果文件大小足够大或堆大小足够低,此代码将愉快地生成java.lang.OutOfMemoryError: Java heap space运行时错误。 显而易见的原因是,它试图将所有通过数据stream(并且实际上已经存储在数据库中)的数据堆叠到所得到的集合中,从而堆起来。

但是,如果用reduce()replacecollect() – 这不会成为问题,因为后者将减less并丢弃所有通过的数据。

在这个例子中,只需用collect()replacecollect()

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

因为Java不是纯粹的FP(函数式编程)语言,并且不能优化stream的底部没有使用的数据,所以甚至不需要考虑使计算取决于result ,效果。

让stream成为<-b <-c <-d

在减less,

你会有((a#b)#c)#d

其中#是你想做的有趣的操作。

在collections中,

你的收集器会有一些收集结构K.

K消耗a。 K然后消耗b。 K然后消耗C。 K然后消耗d。

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

K然后把它给你。