Java 8 Streams-收集与减少


143

什么时候使用collect()vs reduce()?有没有人有好的,具体的例子说明何时最好以一种或另一种方式走?

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

鉴于这是一个可变的减少,我假设它需要同步(内部),这反过来可能对性能造成不利影响。大概reduce()更容易并行化,但需要在精简的每个步骤之后都必须创建新的数据结构以返回以作为代价。

上面的陈述是猜测,不过,我很乐意在此鸣叫。


1
您链接到的页面的其余部分对此进行了解释:与reduce()一样,以这种抽象方式表示collect的好处是它可以直接进行并行化:我们可以并行累积部分结果,然后将它们组合起来,只要累积和合并功能满足相应要求。
JB Nizet 2014年

1
又见“行旅中的Java 8:减少对收集”由安格兰格- youtube.com/watch?v=oWlWEKNM5Aw
MasterJoe2

Answers:


115

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

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

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

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

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

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

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


1
如果是collect的情况:“一种更高效的方法是将结果累积到StringBuilder中”,那么我们为什么还要使用reduce?
jimhooker2002

2
@ Jimhooker2002重读了它。例如,如果要计算乘积,则可以将简化功能简单地并行应用于拆分后的流,然后最后将它们组合在一起。减少的过程总是导致类型为流。收藏是当你想收集结果到一个可变的容器,即当结果是使用不同类型的流。这样做的优点是可以将容器的单个实例用于每个分流,但是缺点是容器需要在最后结合。
蜘蛛鲍里斯(Boris)

1
产品示例中的@ jimhooker2002 int不可变的,因此您不能轻易使用收集操作。您可以像使用AtomicInteger或某些自定义一样进行肮脏的破解,IntWrapper但是为什么呢?折叠操作与收集操作完全不同。
蜘蛛鲍里斯(Boris)

17
还有另一种reduce方法,您可以在其中返回与流元素不同类型的对象。
damluar 2014年

1
您将使用collect而不是reduce的另一种情况是,当reduce操作涉及向集合中添加元素时,则每次累加器函数处理一个元素时,它都会创建一个包含该元素的新集合,这效率很低。
raghu

40

原因很简单:

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

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

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


2
请注意,您使用的是Integer构造函数(new Integer(6)),在更高的Java版本中已弃用。
MC Emperor

1
很好@MCEmperor!我将其更改为Integer.valueOf(6)
Sandro

@Sandro-我很困惑。为什么说collect()仅适用于可变对象?我用它来连接字符串。字符串allNames = employee.stream().map(Employee :: getNameString).collect(Collectors.joining(“,”)).toString();
MasterJoe2

1
@ MasterJoe2很简单。简而言之-实现仍使用StringBuilder可变的。请参阅:hg.openjdk.java.net/jdk8/jdk8/jdk/file/687fd7c7986d/src/share/...
桑德罗

30

正常归约意味着将两个不变值(例如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()

如果你有不可变的值,例如intsdoublesStrings再进行正常降噪工程就好了。但是,如果必须将reduce您的值转换为List可变数据结构,则需要使用该collect方法的可变约简。


在代码片段中,我认为问题在于它将采用身份(在这种情况下为ArrayList的单个实例)并假定它是“不可变的”,因此它们可以启动x线程,每个线程“添加至身份”然后组合在一起。好的例子。
rogerdpack

为什么我们会出现并发修改异常,调用流只会重新运行串行流,这意味着它将由单线程处理,而合并器函数根本不会被调用?
amarnath harish

public static void main(String[] args) { List<Integer> l = new ArrayList<>(); l.add(1); l.add(10); l.add(3); l.add(-3); l.add(-4); List<Integer> numbers = l.stream().reduce( new ArrayList<Integer>(), (List<Integer> l2, Integer e) -> { l2.add(e); return l2; }, (List<Integer> l1, List<Integer> l2) -> { l1.addAll(l2); return l1; });for(Integer i:numbers)System.out.println(i); } }我尝试并没有得到CCm例外
amarnath harish

@amarnathharish当您尝试并行运行它并且多个线程尝试访问同一列表时,会发生问题
乔治

11

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

在减少方面,

您将拥有((a#b)#c)#d

#是您想执行的有趣操作。

在收集中

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

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

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

K然后把它给你。


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

您甚至不需要在乎计算是否依赖于resultJava ,因为Java并不是纯FP(功能性编程)语言,并且由于可能的副作用而无法优化未在流底部使用的数据。 。


3
如果您不关心数据库保存的结果,则应使用forEach ...,无需使用reduce。除非是出于说明目的。
DaveEdelstein '16

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


0

根据文档

当在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如果可能的话,可以稍微提高效率。

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.