悟纤:师傅,我看了前面对于Stream的介绍,但我自己在使用的时候,发现还是有点费劲呢?
师傅:确实Stream要熟练掌握到使用,还是有点难度呐。
悟纤:师傅,那怎么办呐,你的小可爱,还不能完全的理解和掌握。
师傅:小可爱不是还有为师吗,为师这一节打算通过一个实例来展开对于Stream API的讲解。
悟纤:师傅太赞了,为你点赞o( ̄▽ ̄)d。
导读:
前面咱们介绍了Stream API的基本的使用,这一节我们在使用一个实际的例子来对于这个Stream API有更深的理解。
一、例子说明和准备工作
在一个电商系统中,有一个订单类(Order)和商品(Goods)类,每个订单都有年份、商品数量和商品对象属性,而商品类里面则包含了名字和价格属性。
商品Goods类:
/**
* 商品
* @author 悟纤「公众号SpringBoot」
* @date 2022-02-08
* @slogan 大道至简 悟在天成
*/
public class Goods {
//商品名字
private String name;
//商品价格
private double price;
public Goods(String name, double price) {
this.name = name;
this.price = price;
}
public String getName() {
return name;
}
public double getPrice() {
return price;
}
@Override
public String toString() {
return "Goods{" +
"name='" + name + '\'' +
", price='" + price + '\'' +
'}';
}
}
订单类Order:
/**
* 订单
* @author 悟纤「公众号SpringBoot」
* @date 2022-02-08
* @slogan 大道至简 悟在天成
*/
public class Order {
//商品对象
private Goods goods;
//订单日期
private int year;
//商品数量
private int total;
public Order(Goods goods, int year, int total) {
this.goods = goods;
this.year = year;
this.total = total;
}
public Goods getGoods() {
return goods;
}
public int getYear() {
return year;
}
public int getTotal() {
return total;
}
@Override
public String toString() {
return "Order{" +
"goods=" + goods +
", year=" + year +
", total=" + total +
'}';
}
}
最后就是构造出来数据:
//构造商品
Goods bread = new Goods("面包", 10.0);
Goods milk = new Goods("牛奶", 12.0);
Goods juice = new Goods("果汁", 6.0);
Goods ham = new Goods("火腿", 20.0);
//构造订单
List<Order> orders = Arrays.asList(
new Order(ham, 2011, 300),
new Order(bread, 2012, 1000),
new Order(bread, 2011, 400),
new Order(milk, 2012, 710),
new Order(milk, 2012, 700),
new Order(juice, 2012, 950)
);
二、Stream API开战
有了上面的准备工作之后,那么就可以使用Stream API来进行一个统计或者计算了。
2.1 找出2011年所有的订单,按商品数量从低到高排序
看下第一个需求是:找出2011年所有的订单,按商品数量从低到高排序。
第一点要过滤出来2011年的,这个可以使用filter;
第二点是排序,从低到高排序,这个可以使用sorted。
看下代码:
orders.stream() // 1. 构建流
.filter(order->order.getYear()==2011) //2. 中间操作filter和sorted
.sorted(Comparator.comparing(Order::getTotal))
.collect(Collectors.toList()) //3. 终止操作.
.forEach(System.out::println);//输出.
执行结果:
Order{goods=Goods{name='火腿', price='20.0'}, year=2011, total=300}
Order{goods=Goods{name='面包', price='10.0'}, year=2011, total=400}
说明:
(1)filter:filter对原始Stream进行某项测试,通过测试的元素被留下来生成一个新Stream。
(2)sorted:排序函数有两个,一个是用自然顺序排序(不带参数),一个是使用自定义比较器排序(有参数,定义了排序规则)。
2.2获取所有订单里的所有商品的单价
需求:获取所有订单里的所有商品的单价。
orders.stream() // 1.构建流
.map(order->order.getGoods().getPrice()) //2.中间操作,map映射
.collect(Collectors.toSet()) //3.结束操作,转化为Set类型,自动去重
.forEach(System.out::println);//输出
说明:
(1)map:把inputStream的每个元素映射成outputStream的另外一个元素
执行结果:
20.0
10.0
12.0
6.0
2.3查找订单中所有单价为12的商品
需求:查找订单中所有单价为12的商品。
orders.stream()// 1.构建流
.filter(order -> order.getGoods().getPrice()==12) //2.中间操作,filter过滤条件
.distinct() //2. 中间操作:distinct去重
.collect(Collectors.toList())//3.结束操作,转换为List
.forEach(System.out::println);//输出
说明:
(1)filter:filter对原始Stream进行某项测试,通过测试的元素被留下来生成一个新Stream。
(2)distinct:去重
执行结果:
Order{goods=Goods{name='牛奶', price='12.0'}, year=2012, total=710}
Order{goods=Goods{name='牛奶', price='12.0'}, year=2012, total=700}
这里有两条记录,但是是同一个商品信息,对于distinct去重的话,那么完全一样的才会去掉。
如果我们只想留下牛奶(一条记录的话)的话,对于year和total都无视的要怎么搞呢?
一种方式就是重写hashCode()和equals()方法,这里我们就不使用这种方式了,来看下其它的实现方式:
orders.stream()// 1.构建流
.filter(order -> order.getGoods().getPrice()==12) //2.中间操作,filter过滤条件
.map(order -> order.getGoods())//2.中间操作获取满足条件的Goods
.distinct() //2. 中间操作:distinct去重
.collect(Collectors.toList())//3.结束操作,转换为List
.forEach(System.out::println);//输出
执行结果(这里返回类型是Goods):
Goods{name='牛奶', price='12.0'}
2.4查所有商品的名字,拼接成一个符串
需求:查所有商品的名字,拼接成一个符串。
String goodNames = orders.stream()
.map(order -> order.getGoods().name)//获取所有商品的名字
.distinct() //去重
.collect(Collectors.joining(","));//使用joining方法自动拼接
System.out.println(goodNames);
说明:
(1)collect:收集数据,需要指定收集方式,这里指定为了Collectors.joining,数据拼接。
执行结果:
火腿,面包,牛奶,果汁
2.5判断所有订单中是否有价格为20的商品
需求:判断所有订单中是否有价格为20的商品
boolean flag = orders.stream()
.anyMatch(order -> order.getGoods().getPrice()==20);//查询是否有符合条件的元素
System.out.println(flag);
说明:
(1)anyMatch:用于检查流中是否包含具有提供的predicate任何匹配元素。
执行结果:
true
2.6所有订单中商品价格12的商品累计数量
需求:所有订单中商品价格12的商品累计数量
int num1 = orders.stream()
.filter(order -> order.getGoods().getPrice()==12)//筛选出价格12的商品
.map(order -> order.getTotal())//获取价格为12的商品的金额组成的流
.reduce(0,(x,y)->x+y);
System.out.println(num1);
说明:
(1)reduce:用于对stream中元素进行聚合求值,比如:求和,这个可能不是很好理解,待会单独对于reduce进行一个介绍。
另外上面的代码还可以进一步的优化:
int num2 = orders.stream()
.filter(order -> order.getGoods().getPrice()==12)
.map(order -> order.getTotal())
.reduce(0,Integer::sum);//累加所有的金额
System.out.println(num2);
执行结果:
1410
2.7所有订单中商品价格12的商品累计数量
需求:找出所有订单中最大的数量
int max = orders.stream()
.map(Order::getTotal)//获取所有订单的数量
.reduce(0, Integer::max);//找到最大的
System.out.println(max);//1000
执行结果:
1000
2.8找出所有订单中商品数量最小的
需求:找出所有订单中商品数量最小的。
orders.stream()
.map(Order::getTotal)//获取所有订单的数量
.reduce(Integer::min)//找到最小的
.ifPresent(System.out::println);//如果返回的结果不是空就打印
执行结果:
300
三、Stream API之reduce详解
reduce方法用于对stream中元素进行聚合求值,最常见的用法就是将stream中一连串的值合成为单个值,比如为一个包含一系列数值的数组求和。
reduce方法有三个重载的方法,方法签名如下:
Optional<T> reduce(BinaryOperator<T> accumulator);
T reduce(T identity, BinaryOperator<T> accumulator);
<U> U reduce(U identity,
BiFunction<U, ? super T, U> accumulator,
BinaryOperator<U> combiner);
第一个签名方法接受一个BinaryOperator类型的lambada表达式, 常规应用方法如下:
List<Integer> numList = Arrays.asList(1,2,3,4,5);
int result = numList.stream().reduce((a,b) -> a + b ).get();
System.out.println(result);
代码实现了对numList中的元素累加。lambada表达式的a参数是表达式的执行结果的缓存,也就是表达式这一次的执行结果会被作为下一次执行的参数,而第二个参数b则是依次为stream中每个元素。如果表达式是第一次被执行,a则是stream中的第一个元素。
int result1 = numList.stream().reduce((a,b) -> {
System.out.println("a=" + a + ",b=" + b);
return a + b;
} ).get();
在表达式中假如打印参数的代码,打印出来的内容如下:
a=1,b=2
a=3,b=3
a=6,b=4
a=10,b=5
表达式被调用了4次, 第一次a和b分别为stream的第一和第二个元素,因为第一次没有中间结果可以传递, 所以 reduce方法实现为直接将第一个元素作为中间结果传递。
第二个签名的实现:
T reduce(T identity, BinaryOperator<T> accumulator);
与第一个签名的实现的唯一区别是它首次执行时表达式第一次参数并不是stream的第一个元素,而是通过签名的第一个参数identity来指定。我们来通过这个签名对之前的求和代码进行改进:
int result2 = numList.stream().reduce(0,(a,b) ->a+b);
其实这两种实现几乎差别,第一种比第一种仅仅多了一个字定义初始值罢了。此外,因为存在stream为空的情况,所以第一种实现并不直接方法计算的结果,而是将计算结果用Optional来包装,我们可以通过它的get方法获得一个Integer类型的结果,而Integer允许null。第二种实现因为允许指定初始值,因此即使stream为空,也不会出现返回结果为null的情况,当stream为空,reduce为直接把初始值返回。
结束语
好了,对于Stream API的介绍,咱们这里就告一段落了,有兴趣的小伙伴可以根据需要进行学习。
我就是我,是颜色不一样的烟火。
我就是我,是与众不同的小苹果。