前言

本篇博客对应《Java8 in action》一书的第六章,主要总结收集器的操作,在第六章的开头,这本中用一个简单的例子介绍了使用收集器的便捷。这里还是先总结一下这个实例。在原书实例的基础上,我们做了一个改良

需求:对一些交易数据,按照交易年限进行分组

这一些交易数据如下:

@Slf4j
public class TransactionContainer {public static List<Trader> getTraderList(){List<Trader> traderList = new ArrayList<>();//交易员Trader raoul  = new Trader("raoul","Cambridge");Trader mario  = new Trader("mario","Milan");Trader alan  = new Trader("alan","Cambridge");Trader brian  = new Trader("brian","Cambridge");traderList.add(raoul);traderList.add(mario);traderList.add(alan);traderList.add(brian);return traderList;}public static List<Transaction> getTransactionList(){//交易员Trader raoul  = new Trader("raoul","Cambridge");Trader mario  = new Trader("mario","Milan");Trader alan  = new Trader("alan","Cambridge");Trader brian  = new Trader("brian","Cambridge");//交易记录List<Transaction> transactions = Arrays.asList(new Transaction(brian,2011,300),new Transaction(raoul,2012,1000),new Transaction(raoul,2011,400),new Transaction(mario,2012,710),new Transaction(mario,2012,700),new Transaction(alan,2012,950));return transactions;}
}

现在需要按照交易年限分组,分为2011年的交易一组,2012年的交易一组,如果没有收集器我们的操作会是这个样子的

/*** 没有使用collector* @param transactionList*/
public static void outOfUseCollector(List<Transaction> transactionList){Map<Integer,List<Transaction>> transactionByYear = new HashMap<>();for(Transaction transaction:transactionList){int year = transaction.getYear();List<Transaction> transactionYear = transactionByYear.get(year);if(transactionYear == null){transactionYear = new ArrayList<>();transactionByYear.put(year,transactionYear);}transactionYear.add(transaction);}log.info("{}",transactionByYear);
}

繁琐,非常之繁琐。如果有了收集器,我们的操作会是这个样子的

/*** 使用collector* @param transactionList*/
public static void useCollector(List<Transaction> transactionList){Map<Integer, List<Transaction>> result = transactionList.stream().collect(groupingBy(Transaction::getYear));log.info("use collector result : {}",result);
}

还说啥?简单,非常之简单。

这本书中将流收集器的使用分为了几个方面——归约和汇总,分组,分区。我们会一一进行总结

collect(),Collector,Collectors三者的区别

在正式学习这些收集器之前,我们需要区分 collect(),Collector,Collectors三者的区别。

collect是一个方法——java.util.Stream类的内部方法,主要用于将Stream中的数据归约或者分组成另外一种形式。对流调用collect方法将对流中的元素触发一个归约操作(由Collector来参数化)

Collector是一个接口——定义了一些基本的归约和分组操作,这个在后面会详细介绍。一般来说,Collector会对元素应用一个转换函数(很多时候是不体现任何效果的恒等转换,例如toList),并将结果累积在一个数据结构中,从而产生这一过程的最终输出

Collectors是一个工具类——不仅提供了对应的实现类还提供了各种快速生成Collector实例的工具方法。我们这片博客主要就是总结Collectors中的一些方法和操作。Collectors实用类提供了很多静态工厂方法用于进行归约操作

随着整个博客的梳理,后续对这三者的区别会有一个更深的体会。

归约和汇总

归约和汇总指的是将流元素归约和汇总为一个值。这我们还是使用之前的菜单实例来进行操作。在需要将流项目重组成集合时,一般会使用收集器( Stream方法collect的参数)。再宽泛一点来说,但凡要把流中所有的项目合并成一个结果时就可以用。这个结果可以是任何类型,可以复杂如代表一棵树的多级映,或是简单如一个整数——也许代表了菜单的热量总和

Dish实例

package com.learn.stream.common;/*** autor:liman* createtime:2019/8/14* comment: 菜肴*/
public class Dish {private final String name;//姓名private final boolean vegetarian;//是否是蔬菜private final int calories;//热量private final Type type;//类型public Dish(String name, boolean vegetarian, int calories, Type type) {this.name = name;this.vegetarian = vegetarian;this.calories = calories;this.type = type;}public String getName() {return name;}public boolean isVegetarian() {return vegetarian;}public int getCalories() {return calories;}public Type getType() {return type;}public enum Type{MEAT,FISH,OTHER}@Overridepublic String toString() {return "Dish{" +"name='" + name + '\'' +", vegetarian=" + vegetarian +", calories=" + calories +", type=" + type +'}';}
}

菜单实例

public class DishContainer {private static List<Dish> dishList = new ArrayList<>();public static List<Dish> getDishList(){dishList = Arrays.asList(new Dish("pork", false, 800, Dish.Type.MEAT),new Dish("beef", false, 700, Dish.Type.MEAT),new Dish("chicken", false, 400, Dish.Type.MEAT),new Dish("french fries", true, 530, Dish.Type.OTHER),new Dish("rice", true, 350, Dish.Type.OTHER),new Dish("season fruit", true, 120, Dish.Type.OTHER),new Dish("pizza", true, 550, Dish.Type.OTHER),new Dish("prawns", false, 300, Dish.Type.FISH),new Dish("salmon", false, 450, Dish.Type.FISH) );return dishList;}}

统计个数

/*** 测试count的方法——直接统计菜单中有多少个菜*/
public static void testCollectorCount(List<Dish> dishList) {Long dishCount = dishList.stream().collect(Collectors.counting());log.info("discount num : {}", dishCount);//也可以直接写成//dishList.stream().count();
}

查找最大值和最小值

/*** 获取流中的最大值和最小值** @param dishList*/
public static void testGetMaxAndMin(List<Dish> dishList) {//查询菜单中calories最大的dishList.stream().collect(Collectors.maxBy(Comparator.comparingInt(Dish::getCalories))).ifPresent(e -> log.info("max calory dish:{}", e));//查询菜单中calories最小的dishList.stream().collect(Collectors.minBy(Comparator.comparingInt(Dish::getCalories))).ifPresent(e -> log.info("min calory dish:{}", e));
}

汇总

Collectors专门为汇总提供了一些方法

求和

/*** 统计所有的calorie** @param dishList*/
public static void testSummingInt(List<Dish> dishList) {Integer totalCalories = dishList.stream().collect(Collectors.summingInt(Dish::getCalories));log.info("all calories:{}", totalCalories);
}

求平均数

/*** 求平均热量** @param dishList*/
public static void testAverageCalories(List<Dish> dishList) {Double averageCalories = dishList.stream().collect(Collectors.averagingInt(Dish::getCalories));log.info("average calories:{}", averageCalories);
}

到目前为止,你已经看到了如何使用收集器来给流中的元素计数,找到这些元素数值属性的最大值和最小值,以及计算其总和和平均值。不过很多时候,你可能想要得到两个或更多这样的结果,而且你希望只需一次操作就可以完成。在这种情况下,你可以使用summarizingInt工厂方法返回的收集器。

多种结果汇总

IntSummaryStatistics summaryStatistics = dishList.stream().collect(Collectors.summarizingInt(Dish::getCalories));
log.info("summary info : {}",summaryStatistics);

连接字符串

joining工厂方法返回的收集器会把对流中每一个对象应用toString方法得到的所有字符串连接成一个字符串

/*** @param dishList*/
public static void testJoin(List<Dish> dishList) {String allDishName = dishList.stream().map(Dish::getName).collect(Collectors.joining());log.info("all dish name with no spilt : {}", allDishName);//joining方法可以接受元素之间的分界符String allDishNameWithSpilt = dishList.stream().map(Dish::getName).collect(Collectors.joining(", "));log.info("all dish name with spilt:{}", allDishNameWithSpilt);
}

归约和汇总的本质

前面总结的所有的归约操作其实都是reducing操作的特殊情况而已。Collectors.reducing工厂方法是所有这些特殊情况的一般化

我们计算所有菜卡路里的总和还可以采用如下操作

Integer totalCalories = dishList.stream().collect(Collectors.reducing(0, Dish::getCalories, (i, j) -> i + j));

可以直接利用reducing操作,可以看看reducing的源码如下:

public static <T, U> Collector<T, ?, U> reducing(U identity,Function<? super T, ? extends U> mapper,BinaryOperator<U> op) {return new CollectorImpl<>(boxSupplier(identity),(a, t) -> { a[0] = op.apply(a[0], mapper.apply(t)); },(a, b) -> { a[0] = op.apply(a[0], b[0]); return a; },a -> a[0], CH_NOID);
}

可以看到reducing需要三个参数

1、第一个参数是归约操作的起始值,即初始值

2、第二个参数是一个Function<T,R>接口

3、第三个参数是BinaryOperator<T,T,T>接口,这个接口是将两个参数进行汇总的操作

也可以通过以下操作找到热量最高的菜品

Dish dish = dishList.stream().collect(Collectors.reducing((d1, d2) -> d1.getCalories() > d2.getCalories() ? d1 : d2)).get()

分组

简单分组

假设你要把菜单中的菜按照类型进行分类,有肉的放一组,有鱼的放一组,其他的都放另一组。用Collectors.groupingBy工厂方法返回的收集器就可以轻松地完成这项任务,可以如下操作

public static void testSimpleGroupBy(List<Dish> dishList){Map<Dish.Type, List<Dish>> result = dishList.stream().collect(Collectors.groupingBy(Dish::getType));log.info("simple group by result : {}",result);
}

这里,我们给groupingBy方法传递了一个Function(以方法引用的形式),它提取了流中每一道Dish的Dish.Type。我们把这个Function叫作分类函数,因为它用来把流中的元素分成不同的组。

分组操作的结果是一个Map,把分组函数返回的值作为映射的键,把流中所有具有这个分类值的项目的列表作为对应的映射值。

多级分组

多级分组需要一个多参数的groupBy操作,在Collectors类中,提供了两个多参数的groupingBy方法,其中一个具体源码如下

public static <T, K, A, D>
Collector<T, ?, Map<K, D>> groupingBy(Function<? super T, ? extends K> classifier,Collector<? super T, A, D> downstream) {return groupingBy(classifier, HashMap::new, downstream);
}

第二个参数接受Collector类型的参数,而groupBy恰好返回的也是一个Collector类型的参数,这就意味着我们可以直接将groupingBy作为第二个参数传递给上一层的groupingBy方法

/*** 测试多级分组* @param dishList*/
public static void testMultiLevelGroupBy(List<Dish> dishList){//定义底层分组的逻辑Function<Dish,CaloricLevel> deepGroupFunction = dish -> {if(dish.getCalories()<400) return CaloricLevel.DIET;else if(dish.getCalories()<=700) return CaloricLevel.NORMAL;else return CaloricLevel.FAT;};Map<Dish.Type, Map<CaloricLevel, List<Dish>>> deepGroupResult = dishList.stream().collect(Collectors.groupingBy(Dish::getType, Collectors.groupingBy(deepGroupFunction)));log.info("多级分组的结果:{}",deepGroupResult);
}

运行结果示例如下:

{FISH={NORMAL=[Dish{name='salmon', vegetarian=false, calories=450, type=FISH}], DIET=[Dish{name='prawns', vegetarian=false, calories=300, type=FISH}]}, MEAT={NORMAL=[Dish{name='beef', vegetarian=false, calories=700, type=MEAT}, Dish{name='chicken', vegetarian=false, calories=400, type=MEAT}], FAT=[Dish{name='pork', vegetarian=false, calories=800, type=MEAT}]}, OTHER={NORMAL=[Dish{name='french fries', vegetarian=true, calories=530, type=OTHER}, Dish{name='pizza', vegetarian=true, calories=550, type=OTHER}], DIET=[Dish{name='rice', vegetarian=true, calories=350, type=OTHER}, Dish{name='season fruit', vegetarian=true, calories=120, type=OTHER}]}
}

多级分组统计

传递给第一个groupingBy的第二个参数可以是任意的Collector接口,正常的统计类方法也可以,如下所示

public static void testGroupByCount(List<Dish> dishList){Map<Dish.Type, Long> groupByCountResult = dishList.stream().collect(Collectors.groupingBy(Dish::getType, Collectors.counting()));log.info("group by count:{}",groupByCountResult);
}

运行结果

group by count:{FISH=2, MEAT=3, OTHER=4}

再举一个例子

/*** 分组统计,找到每个类型中卡路里最高的蔬菜* @param dishList*/
public static void testGroupByMax(List<Dish> dishList){Map<Dish.Type, Optional<Dish>> maxWithGroupBy = dishList.stream().collect(Collectors.groupingBy(Dish::getType, Collectors.maxBy(Comparator.comparingInt(Dish::getCalories))));log.info("max with group by result : {}",maxWithGroupBy);
}

运行结果

{FISH=Optional[Dish{name='salmon',vegetarian=false,calories=450,type=FISH}],MEAT=Optional[Dish{name='pork',vegetarian=false,calories=800,type=MEAT}],OTHER=Optional[Dish{name='pizza',vegetarian=true,calories=550,type=OTHER}]
}

分区

分区其实是分组的特殊化实例,只是分区接收的是一个返回boolean类型的函数,可以看看如下实例

List<Dish> dishList = DishContainer.getDishList();
Map<Boolean, List<Dish>> result = dishList.stream().collect(Collectors.partitioningBy(Dish::isVegetarian));
log.info("{}",result.get(true));

上述代码中的partitioningBy接受的是一个返回为boolean类型的函数。上述代码运行结果

{false=[Dish{name='pork', vegetarian=false, calories=800, type=MEAT}], true=[Dish{name='french fries', vegetarian=true, calories=530, type=OTHER}]
}

之后我们只需要利用result.get(true)就可以获取到蔬菜了。

上述操作可以通过之前说的filter一样能完成

List<Dish> filterResult = dishList.stream().filter(Dish::isVegetarian).collect(Collectors.toList());

但是分区保留了true和false的关键key值

同样分区也可以对多级,我们操作的一个单参数的partitioningBy函数,其实底层调用的也是接受一个Predicate和Collector参数的方法

public static <T, D, A>
Collector<T, ?, Map<Boolean, D>> partitioningBy(Predicate<? super T> predicate,Collector<? super T, A, D> downstream)

我们上面操作的partitioningBy接口,其实底层也是调用的这个方法

public static <T>
Collector<T, ?, Map<Boolean, List<T>>> partitioningBy(Predicate<? super T> predicate) {return partitioningBy(predicate, toList());
}

因此我们可以partitioningBy进行多级分组

public static void testDeepPartition(){List<Dish> dishList = DishContainer.getDishList();Map<Boolean, Map<Dish.Type, List<Dish>>> result = dishList.stream().collect(Collectors.partitioningBy(Dish::isVegetarian, Collectors.groupingBy(Dish::getType)));log.info("{}",result);
}

Java8实战一书中还介绍了将一堆数分为质数和非质数的操作,具体如下

判断是否是质数

/*** 判断一个数是不是质数** @param candidate* @return*/
public static boolean isPrime(int candidate) {int candidateRoot = (int) Math.sqrt((double) candidate);return IntStream.rangeClosed(2, candidateRoot).noneMatch(i -> candidate % i == 0);//如果一个都没法整除,则是质数
}

将数据进行分组区分

/*** 将数据流分成质数和非质数*/
public static void departPrimeAndNoPrime(int n) {Map<Boolean, List<Integer>> result = IntStream.range(2, n).boxed().collect(Collectors.partitioningBy(candidate -> isPrime(candidate)));log.info("prime result : {}",result.get(true));
}

总结

本篇博客总结了Java8中stream中collect的各种操作,按照《Java8 in action》一书中最后的结尾还自定义了一个Collector的操作,这里不再总结了。

Java8(八)——Stream的collector操作相关推荐

  1. Java8种Stream流相关操作——集合的筛选、归约、分组、聚合

    过滤.筛选   filter skip /*** 过滤 筛选*/@Testpublic void test2(){List<String> list = Arrays.asList(&qu ...

  2. Java8 的 Stream 流式操作之王者归来

    相对于Java8之前的Java的相关操作简直是天差地别,Java8 的流式操作的出现,也很大程度上改变了开发者对于Java的繁琐的操作的印象,从此,Java也走向了函数式编程的道路! 1 流的创建 1 ...

  3. Java8新特性总结 -5.Stream API函数式操作流元素集合

    所有示例代码打包下载 : 点击打开链接 Java8新特性 : 接口新增默认方法和静态方法 Optional类 Lambda表达式 方法引用 Stream API - 函数式操作流元素集合 Date/T ...

  4. Java8的Stream中的Collectors操作求double类型和的坑

    Java8的Stream中的Collectors操作求double类型和的坑 无敌踩坑王的我又双叒叕来了!!!!!! 例子: @Testpublic void testSumDouble() {dou ...

  5. Java8中Stream流对集合操作

    java8中Stream流引入函数式编程思想,主要配合各种接口.lambda表达式.方法引用等方式,为集合的遍历.过滤.映射等提供非常"优雅"的操作方式. Student.java ...

  6. JAVA8 Stream的系列操作,Optional使用---- java养成

    Java养成计划----学习打卡第六十天 内容导航 Stream回顾 Stream的中间操作 筛选和排序 映射 map和flatmap的区别 排序 Stream的终止操作 匹配与查找 归约 收集 Op ...

  7. Java8如何对Stream进行排序操作呢?

    转自: Java8如何对Stream进行排序操作呢? 下文讲述Java8中对Stream进行排序操作的方法分享,如下所示: 实现思路:使用sorted()即可实现对Stream进行排序操作 如:sor ...

  8. 【Java8新特性】关于Java8的Stream API,看这一篇就够了!!

    写在前面 Java8中有两大最为重要的改变.第一个是 Lambda 表达式:另外一个则是 Stream API(java.util.stream.*)  ,那什么是Stream API呢?Java8中 ...

  9. java8的stream特性_Java8新特性介绍:Stream API

    Stream API 了解Stream Java8中有两个比较大的改变 Lambda表达式 Stream API (java.util.stream.*) Stream是Java8中处理集合的关键抽象 ...

最新文章

  1. “十四五”大数据产业发展锚定3万亿目标
  2. UA MATH567 高维统计II 随机向量10 Grothendieck不等式的证明 版本二:kernel trick
  3. BZOJ2093 : [Poi2010]Frog
  4. linux tcp 内核模块,C – Linux – 内核模块 – TCP头
  5. 记录spring、springboot集成apollo配置中心
  6. jsp学习札记————参数传值
  7. HTML下拉菜单为什么无线拉长,【CSS】怎么拉长一个div的高度
  8. Python print 语句(Python 2 与 Python 3)
  9. php 字节码查看,PHP字节码缓存和内置服务器
  10. JAVA 注解示例 详解
  11. 几招让你轻松解决 Uni-app、原生 App 混合开发问题
  12. 不同shp图层合在一起_ps怎么把别的图层的合到一起
  13. 如何正确认识和提升自己的格局
  14. Linux环境下进行本地Blast比对——操作流程
  15. 数据结构笔记(王道考研) 第八章:排序
  16. Linux设备模型-1-主要概念
  17. 豆瓣电影排行榜下载,main主文件代码(未完善版)
  18. 【数学】常用的 证明方法 / 思考方法
  19. Excel制作表格的小技巧
  20. NetConf简介之一篇文章读懂NetConf

热门文章

  1. WPF WinAPI 编程详解(四 实例 )
  2. MS SQLServer 创建数据库关系图
  3. 为Config文件提供Application级别的设置读写
  4. OpenCV边缘检测(二)——Sobel边缘检测
  5. 全志a64linux内核编译,全志A64 lichee编译脚本build.sh分析
  6. 智联招聘上市 互联网钉子户突围
  7. 使用Python批量转换彩色图片到灰度图片
  8. 微信小程序使用云函数操作数据库
  9. SAP script 自动创建销售订单机器人,协同Excel
  10. 什么是度量学习?度量学习如何实现?