160

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

Javadoc 提到 collect() 是一个可变的归约

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

然而,上述陈述是猜测,我希望专家在这里加入。

4

8 回答 8

131

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

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

您链接的文档给出了采用两种不同方法的原因:

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

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

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

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

于 2014-03-22T11:57:09.873 回答
52

原因很简单:

  • 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而只能使用MutableIntfromorg.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);
}

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

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

于 2016-08-02T18:41:00.970 回答
37

正常归约是指结合两个不可变的值,例如 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它不是线程安全的,在迭代时添加/删除值也不安全,因此ArrayIndexOutOfBoundsException当您更新列表或组合器时,您将获得并发异常或任何类型的异常(尤其是并行运行时)尝试合并列表,因为您正在通过累积(添加)整数来改变列表。如果要使此线程安全,则每次都需要传递一个新列表,这会损害性能。

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

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

所以回答你的问题:

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

如果你有诸如ints,之类的不可变值doublesStrings那么正常的归约就可以了。但是,如果您必须将reduce您的值转换为List(可变数据结构),那么您需要在该方法中使用可变归约collect

于 2016-06-01T22:18:52.000 回答
13

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

在减少,

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

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

在收藏中,

您的收集器将具有某种收集结构 K。

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

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

K然后把它给你。

于 2016-10-13T08:27:56.720 回答
3

它们在运行时的潜在内存占用方面非常不同。在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);

您甚至不需要关心使计算依赖于resultJava 不是纯 FP(函数式编程)语言,并且由于可能的副作用而无法优化流底部未使用的数据.

于 2016-07-21T20:12:31.873 回答
3

这是代码示例

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(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函数处理两个参数,第一个参数是流中的上一个返回值,第二个参数是流中的当前计算值,它将第一个值和当前值相加作为下一次计算的第一个值。

于 2018-03-15T15:20:16.790 回答
1

根据文档

在 groupingBy 或 partitioningBy 下游的多级归约中使用时,reducing() 收集器最有用。要对流执行简单的归约,请改用 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.collect 方法来执行此操作,下一节将对此进行描述...

因此,身份在减少场景中被“重用”,因此.reduce如果可能的话,使用效率会更高。

于 2018-02-21T21:17:22.247 回答
0

有一个很好的理由总是更喜欢 collect() 而不是 reduce() 方法。使用 collect() 性能更高,如下所述:

Java 8 教程

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

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

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

于 2020-12-01T03:42:14.883 回答