lambda表达式和StreamAPI实践
java8是个创新的版本,带来很多有用多多特性,其中比较重要的特性就有Lambda表达式和StreamAPI,在过往多项目中,我们也在很多场景下都使用过它们。Lambda表达式特性赋予了java语言函数式编程的能力,让我们可以用简洁优美的语法完成一个功能。概念和原理就不介绍了,下面我们将直接演示我们在项目中会涉及的实践例子。
- 对List数据进行数据筛选
从商品列表获取单个值, 注意findFirst取得的对象要做null值检查
Goods goods = goodsList.stream().filter(x->Objects.equals(x.getGoodsId,1L)).findFirst().orElse(null);
if(goods == null)
{
throw new ShopException();
}
还有一种做法是返回一个可选的Optional对象,Optional也是java8引入的新特性,然后通过get方法拿到我们想要的结果。
Optional<Goods> goodsOptional = goodsList.stream().filter(x->Objects.equals(x.getGoodsId,1L)).findFirst();
if(goodsOptional.isPresent())
{
Goods goods = goodsOptional.get();
}else{
//do something
}
注意下两种情况的不同处理方式,第一种为null直接抛出了异常,表示对象是@NotNull的,第二种不管对象是不是null都有对应的处理逻辑,表示对象是@Nullable的。
- 数据类型转换
从订单商品列表的获取商品id列表示例:
List<Integer> commonIdList = orderGoodsList.stream().map(x->x.getCommonId()).distinct().collect(Collectors.toList());
注意因为订单商品列表中的商品是可以重复的,所以要使用distinct()来去重。
对象数据类型转换
List<String> list = Arrays.asList("1","2","3");
List<Integer> list1 = list.stream().map(Integer::valueOf).collect(Collectors.toList());
list1.forEach(System.out::println);
注意两种map使用方式的区别,本质上都是函数接口,x->x是一个Lambda表达式,Integer::valueOf是一个方法引用,idea会推荐你把Lambda表达式进一步简化为方法引用。
- 数值流求和
获取商品购买数量合计示例:
List<Integer> sum = orderGoodsList.stream().mapInt(x->x.getOrdersGoods().buyNum()).sum();
这个用到了一个mapInt数值流转换方法,类似的还有mapToInt、mapToDouble、mapToLong,数值流汇总方法还有max()、min()、average()、count()等,用法都差不多。
- 去除流中的重复元素
这个方法是通过对象的 equals 方法来判断两个元素是否相等的。
简单类型去重
List<String> list = Arrays.asList("1","2","3");
List<String> listDistinct = list.stream().distinct().collect(Collectors.toList());
listDistinct.forEach(System.out::println);
对象列表去重
需要先重写对象的hashCode()和equals()。
public class Member {
private String memberName = "";
private int age;
@Override
public boolean equals(Object o) {
if (this == o) {
return true;
}
if (!(o instanceof Member)) {
return false;
}
Member member = (Member) o;
if (age != member.age) {
return false;
}
return memberName.equals(member.memberName);
}
@Override
public int hashCode() {
int result = age;
result = 31 * result + memberName.hashCode();
return result;
}
}
去重演示
List<Member> list = new ArrayList<>();
{
list.add(new Member("小明", 15));
list.add(new Member("小明", 15));
list.add(new Member("小李", 26));
list.add(new Member("小李", 26));
list.add(new Member("大黄", 30));
}
long l = list.stream().distinct().count();
System.out.println("No. of distinct members:"+l);
list.stream().distinct().forEach(b -> System.out.println(b.getMemberName()+ "," + b.getAge()));
复杂类型去重
distinct()不提供按照属性对对象列表进行去重的直接实现。如果我们想要按照对象的属性,对对象列表进行去重,我们可以通过其它方法来实现。如下代码段所示:
public static <T> Predicate<T> distinctByKey(Function<? super T, ?> keyExtractor) {
Set<Object> seen = ConcurrentHashMap.newKeySet();
return t -> seen.add(keyExtractor.apply(t));
}
users.stream().filter(distinctByKey(users::getName)).collect(Collectors.toList());
- 对列表进行排序
简单类型正向排序
List<String> list = Arrays.asList("1","2","3");
List<String> listDistinct = list.stream().sorted().collect(Collectors.toList());
listDistinct.forEach(System.out::println);
简单类型逆向排序
List<String> list = Arrays.asList("1","2","3");
List<String> listDistinct = list.stream().sorted(Comparator.reverseOrder()).collect(Collectors.toList());
listDistinct.forEach(System.out::println);
复杂类型正向排序
首先使用 name 排序,紧接着在使用ege 排序,来看下使用效果
users.stream().sorted(Comparator.comparing(User::getAge).thenComparing(User::getName)).collect(Collectors.toList())
users.forEach(System.out::println);
复杂类型逆向排序
users.stream().sorted(Comparator.comparing(User::getAge).reversed().thenComparing(User::getName)).collect(Collectors.toList())
users.forEach(System.out::println);
- 综合案例
业务场景:对用户购买的订单商品按照日期、店铺和区域分组统计。
此案例演示了通过sorted和Comparator.comparing方法对列表进行排序,collect和Collectors.groupingBy方法对订单商品数据进行分组,然后实现商品字段的行转列。
数据库中原始数据如下:
日期 店铺 配送区域 商品名称 购买数量
2018-12-11 浦东店 浦东 鸭蛋 123
2018-12-11 浦东店 浦东 鸡蛋 123
2018-12-11 浦东店 浦东 鹅蛋 123
2018-12-11 浦东店 杨浦 鸭蛋 533
2018-12-11 浦东店 虹口 鸭蛋 533
2018-12-11 青浦店 宝山 鸭蛋 533
2018-12-11 青浦店 静安 鸭蛋 533
对于多条件的分组,最简单的方式就是把多个字段组合成一个key来分组。
Map<String, List<StatOrdersGoodsDayVo>> getListByGroup =
statDaysListCurr.stream().sorted(Comparator.comparing(StatOrdersGoodsDayVo::getCreateTimeDate))
.collect(Collectors.groupingBy(x-> ShopHelper.formatTimestamp(x.getCreateTimeDate(),"yyyy-MM-dd")+"_"+x.getStoreName()+"_"+x.getAreaName()));
通过分组还可以实现对数据表行转列,这上面的例子中,原始数据是订单商品数表,即每行都是一个商品记录,通过分组我们实现了日期、店铺、区域、商品列表的数据结构,在对商品列表进行循环处理,即可实现行转列效果如下表:
日期 店铺 配送区域 鸭蛋 鸡蛋 鹅蛋
2018-12-11 浦东店 浦东 129 600 334
2018-12-11 浦东店 杨浦 22 533 555
2018-12-11 浦东店 虹口 22 533 555
2018-12-11 青浦店 宝山 22 533 555
2018-12-11 青浦店 静安 22 533 555
先构造表头,把商品id和商品名称取出,组合成一个表头map。
List<Object> tHeadList = new ArrayList<>();
for (Integer commonId : commonIdList) {
StatOrdersGoodsSumVo statOrdersGoodsSumVo = statOrdersGoodsSumVoList.stream()
.filter(x->x.getStatOrdersGoods().getCommonId()==commonId).findFirst().orElse(null);
LinkedHashMap<String,Object> goodsMap = new LinkedHashMap<>();
goodsMap.put("commonId",statOrdersGoodsSumVo.getStatOrdersGoods().getCommonId());
goodsMap.put("goodsName",statOrdersGoodsSumVo.getStatOrdersGoods().getGoodsName());
tHeadList.add(goodsMap);
}
行转列示例代码:
List<Object> goodsSumList = new ArrayList<>();
for (Map.Entry<String, List<StatOrdersGoodsDayVo>> entry : getListByGroup.entrySet()) {
String[] temp = entry.getKey().split("_");
String createTimeDate = temp[0];
String storeName = temp[1];
String areaName = temp[2];
HashMap<String, Object> tableTr = new HashMap<>();
tableTr.put("createTimeDate",createTimeDate);
tableTr.put("storeName",storeName);
tableTr.put("areaName",areaName);
//遍历所有商品获取统计值,如果没有商品默认为零
for (Integer commonId : commonIdList) {
StatOrdersGoodsDayVo statOrdersGoodsDayVo = entry.getValue().stream()
.filter(x->x.getStatOrdersGoods().getCommonId() == commonId).findFirst().orElse(null);
if(statOrdersGoodsDayVo == null){
tableTr.put("commonId"+commonId,"0");
}else {
tableTr.put("commonId"+commonId,statOrdersGoodsDayVo.getGoodsBuyNumSum());
}
}
goodsSumList.add(tableTr);
}
行转列是数据库的概念,实际上通过sql的方式也可以很好的实现,以后如果有机会分享sql最佳实践的话再做演示。这里的方式是采用按照商品外的3个字段分组后循环商品列表,然后把"commonId"+commonId做为字段名,商品统计值作为字段值,在前台展示的时候,根据字段名去对应表头的商品名称实现商品统计数据的呈现。需要注意的是表头的商品排序和商品合计的排序要一致
总结
Lambda表达式和StreamAPI的内容远不止这些,在实际的开发中,能应用的场景也还有很多,避免内容太多消化不良,以上只是列了我们目前比较常用的,以后如果有碰到其他场景,我们再补充进来。