Java 8 Streams - 收集与减少

jim*_*002 129 java java-8 java-stream

你什么时候使用collect()vs reduce()?有没有人有一个好的,具体的例子,说明哪种方式更好?

Javadoc提到collect()是一个可变的减少.

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

上面的陈述是猜测,但我喜欢这里的专家.

Bor*_*der 105

reduce它是一个" 折叠 "操作,它将二元运算符应用于流中的每个元素,其中运算符的第一个参数是前一个应用程序的返回值,第二个参数是当前流元素.

collection是一个聚合操作,其中创建"集合"并将每个元素"添加"到该集合.然后将流的不同部分中的集合加在一起.

您链接文档提供了两种不同方法的原因:

如果我们想要获取字符串流并将它们连接成一个长字符串,我们可以通过普通减少来实现:

 String concatenated = strings.reduce("", String::concat)  
Run Code Online (Sandbox Code Playgroud)

我们会得到理想的结果,甚至可以并行工作.但是,我们可能对性能不满意!这样的实现将进行大量的字符串复制,并且运行时间将是字符数的O(n ^ 2).更高效的方法是将结果累积到StringBuilder中,StringBuilder是用于累积字符串的可变容器.我们可以使用相同的技术来并行化可变缩减,就像我们使用普通缩减一样.

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

  • 还有另一个`reduce`方法,您可以返回与流元素不同类型的对象. (15认同)
  • @Jimhooker2002 重读一遍。例如,如果您要计算乘积,那么可以简单地将归约函数并行应用于拆分的流,然后在最后组合在一起。减少的过程总是导致类型为流。当您想将结果收集到可变容器中时使用收集,即当结果是流的 _different_ 类型时。这具有容器的**单个实例**可用于每个拆分流的优点,但缺点是容器需要在最后组合。 (2认同)

San*_*dro 37

原因很简单:

  • 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);
}
Run Code Online (Sandbox Code Playgroud)

" collect()与可变"的例子

例如,如果你想使用手动计算总和collect()不能一起工作BigDecimal,但只有MutableIntorg.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);
}
Run Code Online (Sandbox Code Playgroud)

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

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

  • 请注意,您使用的是Integer构造函数(新的Integer(6)),在更高的Java版本中不推荐使用。 (2认同)

geo*_*rge 24

正常的缩减是为了组合两个不可变的值,如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; });
Run Code Online (Sandbox Code Playgroud)

这相当于Collectors.toList().但是,在这种情况下你改变了List<Integer>.我们知道这ArrayList不是线程安全的,在迭代时添加/删除它的安全性也是不安全的,因此当你更新列表或者更新列表时,你将获得并发异常或arrayIndexOutBound异常或任何类型的异常(特别是当并行运行时)组合器尝试合并列表,因为您通过累积(添加)整数来改变列表.如果你想使这个线程安全,你需要每次都传递一个新列表,这会影响性能.

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

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

所以回答你的问题:

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

如果你有不可变的值,例如ints,doubles,Strings再进行正常降噪工程就好了.但是,如果你必须将reduce你的值称为List(可变数据结构),那么你需要使用该collect方法进行可变缩减.


小智 8

让流为< - b < - c < - d

在减少,

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

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

在收集中,

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

K消耗了一个.然后K消耗b.然后K消耗c.K然后消耗d.

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

K然后给你.