【问题标题】:Java 8 Streams - collect vs reduceJava 8 Streams - 收集与减少
【发布时间】:2014-04-29 21:48:33
【问题描述】:

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

Javadoc mentions that collect() is a mutable reduction.

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

不过,以上陈述都是猜测,我希望专家能在此插话。

【问题讨论】:

  • 您链接到的页面的其余部分对此进行了解释:与 reduce() 一样,以这种抽象方式表达 collect 的一个好处是它直接适合并行化:我们可以累积部分将结果并行然后合并,只要累加和合并函数满足相应的要求即可。
  • 另见 Angelika Langer 的“Java 8 中的流:减少与收集” - youtube.com/watch?v=oWlWEKNM5Aw

标签: java java-8 java-stream


【解决方案1】:

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

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

document you linked 给出了采用两种不同方法的原因:

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

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

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

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

【讨论】:

  • 如果collect是这种情况:“一种更高效的方法是将结果累积到StringBuilder中”那我们为什么要使用reduce?
  • @Jimhooker2002 重读它。例如,如果您要计算乘积,则可以简单地将归约函数并行应用于拆分流,然后在最后组合在一起。减少的过程总是导致类型为流。当您想要将结果收集到可变容器中时使用收集,即当结果是流的 不同 类型时。这样做的好处是容器的单个实例可以用于每个拆分流,但缺点是容器需要在最后组合。
  • @jimhooker2002 在产品示例中,int不可变的,因此您不能轻易使用收集操作。你可以做一个肮脏的黑客攻击,比如使用AtomicInteger 或一些自定义的IntWrapper,但你为什么要这样做?折叠操作与收集操作完全不同。
  • 还有另一个reduce 方法,您可以在其中返回与流元素不同类型的对象。
  • 你会使用 collect 而不是 reduce 的另一种情况是,当 reduce 操作涉及向集合中添加元素时,每次你的 accumulator 函数处理一个元素时,它都会创建一个包含该元素的新集合,这是低效的。
【解决方案2】:

原因很简单:

  • 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 一起使用,而只能与来自org.apache.commons.lang.mutableMutableInt 一起使用。见:

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()); 不应该返回带有结果的新对象,而是更改 MutableInt 类型的可变 container 的状态。

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

【讨论】:

  • 请注意,您使用的是 Integer 构造函数 (new Integer(6)),在以后的 Java 版本中已弃用。
  • 好消息@MCEmperor!我已将其更改为Integer.valueOf(6)
  • @Sandro - 我很困惑。为什么你说 collect() 只适用于可变对象?我用它来连接字符串。 String allNames = employees.stream() .map(Employee::getNameString) .collect(Collectors.joining(", ")) .toString();
  • @MasterJoe2 这很简单。简而言之 - 实现仍然使用可变的StringBuilder。见:hg.openjdk.java.net/jdk8/jdk8/jdk/file/687fd7c7986d/src/share/…
【解决方案3】:

正常归约是指将两个不可变值组合起来,例如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&lt;Integer&gt;。正如我们所知,ArrayList 不是线程安全的,在迭代时从其中添加/删除值也不安全,因此当您遇到并发异常或ArrayIndexOutOfBoundsException 或任何类型的异常时(尤其是并行运行时)更新列表或组合器尝试合并列表,因为您正在通过累积(添加)整数来改变列表。如果你想让这个线程安全,你需要每次都传递一个新列表,这会影响性能。

相比之下,Collectors.toList() 的工作方式类似。但是,当您将值累积到列表中时,它可以保证线程安全。来自documentation for the collect method

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

所以回答你的问题:

您什么时候使用collect()reduce()

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

【讨论】:

  • 在代码 sn-p 中,我认为问题在于它将获取标识(在本例中为 ArrayList 的单个实例)并假设它是“不可变的”,因此他们可以启动 x 线程,每个“添加到身份”然后组合在一起。很好的例子。
  • 为什么我们会得到并发修改异常,调用streams只是要重新运行串行流,这意味着它会被单线程处理并且根本不调用组合函数?
  • public static void main(String[] args) { List&lt;Integer&gt; l = new ArrayList&lt;&gt;(); l.add(1); l.add(10); l.add(3); l.add(-3); l.add(-4); List&lt;Integer&gt; numbers = l.stream().reduce( new ArrayList&lt;Integer&gt;(), (List&lt;Integer&gt; l2, Integer e) -&gt; { l2.add(e); return l2; }, (List&lt;Integer&gt; l1, List&lt;Integer&gt; l2) -&gt; { l1.addAll(l2); return l1; });for(Integer i:numbers)System.out.println(i); } } 我试过并没有得到 CCm 异常
  • @amarnathharish 当您尝试并行运行它并且多个线程尝试访问同一个列表时会出现问题
【解决方案4】:

让流为a

在减少,

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

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

收藏中,

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

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

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

K 然后给你。

【讨论】:

    【解决方案5】:

    它们在运行时的潜在内存占用方面非常不同。 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(函数式编程)语言,并且无法优化流底部未使用的数据,因为可能的副作用。

    【讨论】:

    • 如果你不关心你的数据库保存的结果,你应该使用forEach...你不需要使用reduce。除非这是为了说明目的。
    【解决方案6】:

    这是代码示例

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

    【讨论】:

      【解决方案7】:

      根据the docs

      reducing() 收集器在用于多级归约、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 tutorialreduce 有时效率较低

      reduce 操作总是返回一个新值。但是,累加器函数在每次处理流的元素时也会返回一个新值。假设您要将流的元素简化为更复杂的对象,例如集合。这可能会妨碍您的应用程序的性能。如果您的 reduce 操作涉及将元素添加到集合中,那么每次您的累加器函数处理一个元素时,它都会创建一个包含该元素的新集合,这是低效的。相反,更新现有集合对您来说会更有效。您可以使用 Stream.collect 方法执行此操作,下一节将对此进行介绍...

      所以身份在 reduce 场景中被“重复使用”,所以如果可能的话,使用.reduce 会更有效。

      【讨论】:

        【解决方案8】:

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

        Java 8 tutorial

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

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

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

        【讨论】:

          猜你喜欢
          • 2015-02-24
          • 2019-02-17
          • 2015-06-19
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 2019-04-29
          • 1970-01-01
          • 1970-01-01
          相关资源
          最近更新 更多