繁体   English   中英

Java 8 Streams - 收集与减少

[英]Java 8 Streams - collect vs reduce

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

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

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

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

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

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

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

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

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

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

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

原因很简单:

  • 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.mutable MutableInt一起使用。 看:

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

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

正常的归约是将两个不可变的值(例如 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()reduce()

如果您有不可变的值,例如intsdoublesStrings那么正常的缩减就可以了。 但是,如果您必须将值reduceList (可变数据结构),那么您需要使用带有collect方法的可变缩减。

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

在减少,

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

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

在收藏中,

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

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

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

K然后把它给你。

它们在运行时的潜在内存占用非常不同。 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 runtime error,如果文件大小足够大或堆大小足够小。 显而易见的原因是它试图将所有通过流(实际上已经存储在数据库中)的数据堆叠到结果集合中,这会炸毁堆。

但是,如果您将collect()替换为reduce() - 它不再是问题,因为后者将减少并丢弃所有通过的数据。

在给出的示例中,只需将collect()替换为带有reduce

.reduce(0L, (aLong, result) -> aLong, (aLong1, aLong2) -> aLong1);

您甚至不需要关心使计算依赖于result因为 Java 不是纯 FP(函数式编程)语言,并且由于可能的一面,无法优化流底部未使用的数据 -效果。

这是代码示例

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

根据文档

当在 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 操作总是返回一个新值。 但是,累加器函数每次处理流的元素时也会返回一个新值。 假设您要将流的元素减少为更复杂的对象,例如集合。 这可能会影响应用程序的性能。 如果您的reduce 操作涉及向集合添加元素,那么每次您的累加器函数处理一个元素时,它都会创建一个包含该元素的新集合,这是低效的。 改为更新现有集合会更有效。 您可以使用 Stream.collect 方法执行此操作,下一节将介绍该方法...

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

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

Java 8 教程

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

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

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

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM