Java 8 Streams – 收集vs减less
你什么时候使用collect()
vs reduce()
? 有没有人有一个很好的具体的例子,什么时候最好走一条路?
Javadoc提到collect()是一个可变的减less 。
鉴于这是一个可变的减less,我认为它需要同步(内部),这反过来可能是有害的performance。 大概reduce()
是更容易并行化的代价是必须创build一个新的数据结构,以减less每一步之后的返回。
以上的陈述是猜测,但我很乐意专家在这里钟声。
首先,返回值是不同的:
<R,A> R collect(Collector<? super T,A,R> collector) T reduce(T identity, BinaryOperator<T> accumulator)
所以collect
返回任何R
而reduce
回报T
– Stream
的types。
reduce
是一个“ 折叠 ”操作,它将二元运算符应用到stream中的每个元素,其中第一个参数是前一个应用程序的返回值,第二个参数是当前stream元素。
collection
是一个聚集操作,其中创build“集合”,并将每个元素“添加”到该集合中。 然后把stream中不同部分的集合加在一起。
你链接的文件给出了两种不同方法的理由:
如果我们想要把一串string连接起来,并把它们连接成一个单一的长string,我们可以用普通的还原来实现:
String concatenated = strings.reduce("", String::concat)
我们会得到期望的结果,甚至可以并行工作。 但是,我们可能不会对表演感到高兴! 这样的实现会进行大量的string复制,运行时间将会是字符数量的O(n ^ 2)。 更高效的方法是将结果累积到一个StringBuilder中,这是一个用于累加string的可变容器。 我们可以使用相同的技术来平行化可变的缩减,就像我们用普通的缩减一样。
所以重点在于两种情况下的并行化是相同的,但是在reduce
情况下,我们将这个函数应用到stream元素本身。 在collect
情况下,我们把这个函数应用到一个可变的容器中。
原因很简单:
-
collect()
只能使用可变结果对象。 -
reduce()
被devise为与不可变的结果对象一起工作 。
“ 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(new Integer(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); }
这是因为accumulator container.add(employee.getSalary().intValue());
不应该返回一个带有结果的新对象,而是改变MutableInt
types的可变container
的MutableInt
。
如果您想使用BigDecimal
代替container
,则不能使用collect()
方法作为container.add(employee.getSalary());
不会更改container
因为BigDecimal
是不可变的。 (除了这个BigDecimal::new
不会工作,因为BigDecimal
没有空的构造函数)
正常的减less是为了结合两个不可变的值,如int,double等,并产生一个新的; 这是一个不可改变的减less。 相比之下,收集方法的目的是改变一个容器,以积累它应该产生的结果。
为了说明这个问题,让我们假设你想用下面的简单归约实现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
不是线程安全的,在迭代时添加/删除值也不是安全的,所以当您更新列表时,您将获得并发exception或arrayIndexOutBoundexception或任何types的exception(特别是在并行运行时)或者组合器尝试合并列表,因为您正在通过累积(添加)整数来改变列表。 如果你想使这个线程安全,你需要每次都传递一个新的列表,这会损害性能。
相反, Collectors.toList()
以类似的方式工作。 但是,当您将值累加到列表中时,它会保证线程安全。 从collect
方法的文档:
使用收集器对此stream的元素执行可变减less操作。 如果stream是并行的,并且收集器是并发的,并且stream是无序的或者收集器是无序的,那么将执行并发的减less。 当并行执行时,可以实例化,填充和合并多个中间结果,以保持可变数据结构的隔离。 因此,即使与非线程安全的数据结构(如ArrayList)并行执行,并行还原也不需要额外的同步。 链接
所以要回答你的问题:
你什么时候使用
collect()
vsreduce()
?
如果你有不可改变的值如ints
, doubles
, Strings
那么正常的还原就可以了。 然而,如果你不得不把你的值reduce
到一个List
(可变数据结构),那么你需要使用collect
方法使用可变的减less。
它们在运行时潜在的内存占用差异很大。 collect()
收集所有数据并将其放入集合,而reduce()
明确要求您指定如何减less通过stream的数据。
例如,如果你想从一个文件中读取一些数据,处理它,并把它放到一个数据库中,你可能会得到类似于这样的javastream代码:
streamDataFromFile(file) .map(data -> processData(data)) .map(result -> database.save(result)) .collect(Collectors.toList());
在这种情况下,我们使用collect()
来强制Java通过数据stream,并将结果保存到数据库中。 如果没有collect()
,数据永远不会被读取,也不会被存储。
如果文件大小足够大或堆大小足够低,此代码将愉快地生成java.lang.OutOfMemoryError: Java heap space
运行时错误。 显而易见的原因是,它试图将所有通过数据stream(并且实际上已经存储在数据库中)的数据堆叠到所得到的集合中,从而堆起来。
但是,如果用reduce()
replacecollect()
– 这不会成为问题,因为后者将减less并丢弃所有通过的数据。
在这个例子中,只需用collect()
replacecollect()
:
.reduce(0L, (aLong, result) -> aLong, (aLong1, aLong2) -> aLong1);
因为Java不是纯粹的FP(函数式编程)语言,并且不能优化stream的底部没有使用的数据,所以甚至不需要考虑使计算取决于result
,效果。
让stream成为<-b <-c <-d
在减less,
你会有((a#b)#c)#d
其中#是你想做的有趣的操作。
在collections中,
你的收集器会有一些收集结构K.
K消耗a。 K然后消耗b。 K然后消耗C。 K然后消耗d。
最后,你问K最后的结果是什么。
K然后把它给你。