使用Stream API在内存中处理数据
介绍Map-Filter-Reduce
(映射-过滤-归约)算法
这是非常经典的处理数据的算法。下面是一个销售类
public class Sale {
private String product;
private LocalDate date;
private int amount;
// constructors, getters, setters
// equals, hashCode, toString
}
假设需要计算出三月分销售的总价
List<Sale> sales = ...; // this is the list of all the sales
int amountSoldInMarch = 0;
for (Sale sale: sales) {
if (sale.getDate().getMonth() == Month.MARCH) {
amountSoldInMarch += sale.getAmount();
}
}
System.out.println("Amount sold in March: " + amountSoldInMarch);
- 过滤出3月份的销售
- 提取销售类中的
amount
属性,即映射Sale
到amount
- 求销售总额,即将各个销售额归约为总销售额
类似SQL语言
select sum(amount)
from Sales
where extract(month from date) = 3;
指定结果代替编程式算法(命令式编程vs声明式编程)
Stream API 的两个目标是使您能够创建更具可读性和表现力的代码,并为 Java 运行时提供一些摆动空间来优化您的计算。
将对象映射到其他对象或值
Map-Filter-Reduce
(映射-过滤-归约)算法中映射,映射是一对一的转换:如果您映射一个包含 10 个对象的列表,您将得到一个包含 10 个转换对象的列表。
映射可以改变对象类型,但不改变对象的顺序
映射由Function
函数式接口建模。
过滤掉对象
过滤并不能触及改变对象,仅仅决定是否选择或者删除对象。
过滤可以改变对象的顺序,但不能改变对象的类型
过滤由Predicate
函数式接口建模。
由对象归约产生结果
归约类似于SQL中的聚合。类如COUNT
, SUM
, MIN
, MAX
, AVERAGE
。Stream API 支持所有这些聚合。
归约允许通过你的数据构建出复杂的数据结构,包括List
, Set
, Map
或者你自己构建的结构。
优化 Map-Filter-Reduce
算法
统计超过100k人口城市的总人口
不使用Stream API
List<City> cities = ...;
int sum = 0;
for (City city: cities) {
int population = city.getPopulation();
if (population > 100_000) {
sum += population;
}
}
System.out.println("Sum = " + sum);
使用伪代码
int sum = cities.map(city -> city.getPopulation())
.filter(population -> population > 100_000)
.sum();
考虑伪代码的返回值
Collection<Integer> populations = cities.map(city -> city.getPopulation());
Collection<Integer> filteredPopulations = populations.filter(population -> population > 100_000);
int sum = filteredPopulations.sum();
链式调用可读性更高,但上面伪代码也是正确的。
分析上面的代码:
- 第一步映射,如果你想处理1000个
City
集合,则处理提取City
其中population
属性映射到1000个population
属性集合中。 - 第二部过滤,过滤掉集合中100k人口以下的
population
属性。
存储中间集合结果会有大量的内存开销,特别是当要处理的数据集合很大的时候。使用循环编码的话就没有这方面的开销,它将结果直接累加并不存储在中间集合中。
为避免这种开销,正确的模式:
Stream<City> streamOfCities = cities.stream();
Stream<Integer> populations = streamOfCities.map(city -> city.getPopulation());
Stream<Integer> filteredPopulations = populations.filter(population -> population > 100_000);
int sum = filteredPopulations.sum(); // in fact this code does not compile; we'll fix it later
Stream
接口避免了创建中间结构用于存储映射、过滤的结果。map()
和filter()
方法依旧返回新的Stream
对象。
Stream
对象不存储任何数据
Stream API 的设计方式是,只要您不在stream流模式中创建任何非Stream
对象,就不会对您的数据进行计算。
stream流处理操作如同上面循环操作一样,没有额外的内存消耗。
使用stream流是关于创建操作pipeline管道。在某些时候,您的数据将通过此管道传输并被转换、过滤,然后将参与结果的产生。
pipeline管道由stream流上的一系列方法调用组成。每个调用都会产生另一个stream流。然后在某个时候,最后一次调用会产生结果。返回另一个stream流的操作称为中间操作。另一方面,返回其他内容(包括 void)的操作称为终端操作。
使用中间操作创建pipeline管道
中间操作是返回另一个流的操作。调用这样的操作会在现有的操作管道上增加一项操作,而无需处理任何数据。它由返回stream流的方法建模。
使用终端操作计算结果
终端操作是不返回stream流的操作。调用这样的操作会触发stream流源元素的消耗。然后这些元素由中间操作的pipeline管道处理,一次一个元素。
终端操作由一个方法建模,该方法返回除stream流以外的任何内容,包括 void。
您不能在stream流上调用多个终端方法。如果这样做,您将收到带有以下消息的 IllegalStateException
:“流已被操作或关闭”。
使用专门的数字Stream流避免装箱
Stream API 为您提供了四个接口。
Stream
IntStream
LongStream
DoubleStream
特殊的终端操作
sum()
min()
,max()
average()
summaryStatistics()