Java 8 Streams - 收集与减少

时间:2014-03-22 11:49:46

标签: java java-8

您何时会使用collect() vs reduce()?有没有人有一个好的,具体的例子,说明什么时候选择这种方式肯定会更好?

Javadoc mentions that collect() is a mutable reduction

鉴于它是一个可变的减少,我认为它需要同步(内部),这反过来可能对性能有害。据推测,reduce()更容易并行化,代价是必须在reduce中的每个步骤之后创建一个新的数据结构。

然而,上述陈述是猜测,我希望有一位专家在这里发声。

8 个答案:

答案 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); } 旨在与不可变结果对象一起使用

&#34; collect()使用不可变的&#34;示例

collect()

&#34; 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() vs reduce()

如果您有intsdoublesStrings等不可变值,那么正常缩减就可以了。但是,如果您必须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()的性能要好得多,如下所述:

Java 8 tutorial

*一个可变的约简操作(例如Stream.collect())在处理流元素时将其收集在一个可变结果容器(collection)中。 与不变的约简操作(例如Stream.reduce())相比,可变的约简操作提供了更高的性能。

这是由于以下事实:保存在每个归约步骤中的结果的集合对于收集器来说是可变的,并且可以在下一步中再次使用。

另一方面,

Stream.reduce()操作使用不可变的结果容器,因此需要在每个还原的中间步骤实例化容器的新实例,从而降低性能。*

相关问题