Java中的Stream流收集器

目录

1、归约和汇总

2、分组

3、分区

4、理解收集器接口


        Java Stream 流用来帮助处理集合,类似于数据库中的操作。

        在 Stream 接口中,有一个抽象方法 collect,你会发现 collect 是一个归约操作(高级规约),就像 reduce 一样可以接受各种做法作为参数,将流中的元素累积成一个汇总结果。具体的做法可以通过Collector 接口来定义。// collect和reduce都是Stream接口中的汇总方法,collect方法接收一个Collector(收集器)作为参数

        收集器(Collector)非常有用,Collector 接口中方法的实现决定了如何对流执行归约操作//收集器用来定义收集器如何收集数据

        一般来说,Collector 会对元素应用一个转换函数,并将结果累积在一个数据结构中,从而产生这一过程的最终输出。

        Collectors 实用类提供了很多静态工厂方法,可以方便地创建常见收集器的实例,所以只要拿来用就可以了。最直接和最常用的收集器比如 toList、toSet、toMap 等静态方法。

1、归约和汇总

        使用 Collectors 进行规约和汇总,比如统计汇总,查找最大值和最小值,拼接字符串等。

        本节示例会使用到如下代码:

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 name;}public static final List<Dish> menu = Arrays.asList(new Dish("pork", false, 800, Type.MEAT), new Dish("beef", false, 700, Type.MEAT), new Dish("chicken", false, 400, Type.MEAT), new Dish("french fries", true, 530, Type.OTHER), new Dish("rice", true, 350, Type.OTHER), new Dish("season fruit", true, 120, Type.OTHER), new Dish("pizza", true, 550, Type.OTHER), new Dish("prawns", false, 400, Type.FISH), new Dish("salmon", false, 450, Type.FISH));
}

        汇总操作:Collectors 类专门为汇总提供了一些工厂方法,用来满足各种类型的统计需要。

    public static void main(String[] args) {//1、计数Long count0 = menu.stream().collect(Collectors.counting());long count1 = menu.stream().count();//2、汇总和Integer summingInt = menu.stream().collect(Collectors.summingInt(Dish::getCalories));//3、求平均数Double averagingInt = menu.stream().collect(Collectors.averagingInt(Dish::getCalories));//4、一次性求:计数量、总和、最大值、最小值、平均值IntSummaryStatistics statistics = menu.stream().collect(Collectors.summarizingInt(Dish::getCalories));//使用统计后的值double average = statistics.getAverage();long sum = statistics.getSum();int max = statistics.getMax();int min = statistics.getMin();long count = statistics.getCount();}

        上述示例中,使用 summarizingInt 工方法返回的收集器功能很强大,通过一次 summarizing 操作你就可以统计到元素的个数,并得到总和、平均值、最大值和最小值

        查找流中的最大值和最小值Collectors.maxBy Collectors.minBy,来计算流中的最大或最小值。这两个收集器接收一个 Comparator 参数来比较流中的元素。

    public static void main(String[] args) {//比较器Comparator<Dish> comparator = Comparator.comparing(Dish::getCalories);//收集器Optional<Dish> collect = menu.stream().collect(Collectors.maxBy(comparator));}

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

String joining = menu.stream().map(Dish::getName).collect(Collectors.joining(",")); //joining:
pork,beef,chicken,french fries,rice,season fruit,pizza,prawns,salmon

        广义的规约汇总:事实上,前边所有的收集器,都是一个可以用 reducing 工厂方法定义的归约过程的特殊情况而已Collectors.reducing 工厂方法是所有这些特殊情况的一般化。如下所示:

    public static void main(String[] args) {//1、计数Long count0 = menu.stream().collect(Collectors.counting());Integer count1 = menu.stream().map(i -> 1).collect(Collectors.reducing(0, (a, b) -> a + b));//2、汇总和Integer summingInt = menu.stream().collect(Collectors.summingInt(Dish::getCalories));Integer sum = menu.stream().map(Dish::getCalories).collect(Collectors.reducing(0, (a, b) -> a + b));//3、求最大值Optional<Dish> max0 = menu.stream().collect(Collectors.maxBy(Comparator.comparing(Dish::getCalories)));Optional<Dish> max1 = menu.stream().collect(Collectors.reducing((a, b) -> a.calories > b.calories ? a : b));}

        到此,你可能想知道,Stream接口的 collect 和 reduce 方法有何不同?因为两种方法通常会获得相同的结果。

        这个区别在于,reduce 方法旨在把两个值结合起来生成一个新值,它是一个不可变的归约。与此相反,collect 方法的设计就是要改变容器,从而累积要输出的结果。//语义上的区别

        怎么理解这个区别呢?来看一段滥用 reduce 方法的代码:

    public static void main(String[] args) {Stream<Integer> stream = Arrays.asList(1, 2, 3, 4, 5, 6).stream();List<Integer> numbers = stream.reduce(new ArrayList<Integer>(), //1、初始值:List(List<Integer> list, Integer e) -> { //2、转换函数:将Integer变为->Listlist.add(e);return list;},(List<Integer> list1, List<Integer> list2) -> {//3、累计函数:多个List合并成一个Listlist1.addAll(list2);return list1;});System.out.println(numbers); //[1, 2, 3, 4, 5, 6]}

        上面的代码片段是在滥用 reduce 方法,因为它在原地改变了作为累加器的 List。//不能说上边方法错误,但是看起来有点炫技的意思,代码很复杂,做的事情却很少,而且使用并发流时还会存在线程安全问题

        同样的问题,对比下 collect 方法的实现,你会发现非常的简单:

    public static void main(String[] args) {Stream<Integer> stream = Arrays.asList(1, 2, 3, 4, 5, 6).stream();List<Integer> numbers = stream.collect(Collectors.toList());}

        一般来说,函数式编程通常会提供了多种方法来执行同一个操作。所以,我们需要根据情况选择最佳解决方案。收集器在某种程度上比 Stream 接口上直接提供的方法用起来更复杂,但好处在于它们能提供更高水平的抽象和概括,也更容易重用和自定义。

2、分组

        数据库中一个常见操作是根据一个或多个属性对集合中的项目进行分组。

        简单分组:用 Collectors.groupingBy 工厂方法返回的收集器就可以轻松地完成这项任务,如下所示:

public static void main(String[] args) {//1、简单分组Map<Type, List<Dish>> grouping0 = menu.stream().collect(Collectors.groupingBy(Dish::getType));//2、自定义分组条件Map<String, List<Dish>> grouping1 = menu.stream().collect(Collectors.groupingBy(r -> {if (r.getCalories() <= 400) {return "低热量";} else if (r.getCalories() <= 700) {return "普通";} else {return "高热量";}}));System.out.println(grouping1);}//1、简单分组
{MEAT=[pork, beef, chicken], OTHER=[french fries, rice, season fruit, pizza], FISH=[prawns, salmon]}
//2、自定义分组条件
{普通=[beef, french fries, pizza, salmon], 低热量=[chicken, rice, season fruit, prawns], 高热量=[pork]}

        多级分组:Collectors.groupingBy 工厂方法创建的收集器,它除了普通的分类函数之外,还可以接受 Collector 类型的第二个参数。那么要进行多级分组的话,就可以把一个内层 groupingBy 传递给外层 groupingBy,如下所示:

    public static void main(String[] args) {Map<Type, Map<String, List<Dish>>> grouping = menu.stream().collect(Collectors.groupingBy(Dish::getType, //一级分类函数Collectors.groupingBy(r -> { //二级分类函数if (r.getCalories() <= 400) {return "低热量";} else if (r.getCalories() <= 700) {return "普通";} else {return "高热量";}})));
//分组:
{
FISH={普通=[salmon], 低热量=[prawns]}, 
MEAT={普通=[beef], 低热量=[chicken], 高热量=[pork]}, 
OTHER={普通=[french fries, pizza], 低热量=[rice, season fruit]}
}

        按子组收集数据:上边可以把第二个 groupingBy 收集器传递给外层收集器来实现多级分组。但是,传递给第一个 groupingBy 的第二个收集器可以是任何类型,而不一定是另一个 groupingBy,如下所示:

    public static void main(String[] args) {//1、简单分组Map<Type, List<Dish>> groupingBy = menu.stream().collect(Collectors.groupingBy(Dish::getType));//2、分组后计数Map<Type, Long> collect0 = menu.stream().collect(Collectors.groupingBy(Dish::getType,Collectors.counting()));//3、分组后获取最大值Map<Type, Optional<Dish>> collect1 = menu.stream().collect(Collectors.groupingBy(Dish::getType,Collectors.maxBy(Comparator.comparing(Dish::getCalories))));//4、分组后获取最大值Map<Type, Dish> collect2 = menu.stream().collect(Collectors.groupingBy(Dish::getType,Collectors.collectingAndThen(Collectors.maxBy(Comparator.comparing(Dish::getCalories)), Optional::get)));//5、分组后按名称倒序排序Map<Type, List<Dish>> collect3 = menu.stream().collect(Collectors.groupingBy(Dish::getType,Collectors.collectingAndThen(Collectors.toList(),list -> list.stream().sorted(Comparator.comparing(Dish::getName).reversed()).collect(Collectors.toList()))));//6、分组后更该返回映射Map<Type, Set<String>> collect4 = menu.stream().collect(Collectors.groupingBy(Dish::getType, Collectors.mapping(r -> {if (r.getCalories() <= 400) {return "低热量";} else if (r.getCalories() <= 700) {return "普通";} else {return "高热量";}}, Collectors.toSet())));}//1、简单分组:{MEAT=[pork, beef, chicken], OTHER=[french fries, rice, season fruit, pizza], FISH=[prawns, salmon]}
//2、分组后计数:{MEAT=3, OTHER=4, FISH=2}
//3、分组后获取最大值:{OTHER=Optional[pizza], FISH=Optional[salmon], MEAT=Optional[pork]}
//4、分组后获取最大值:{OTHER=pizza, FISH=salmon, MEAT=pork}
//5、分组后按名称倒序排序:{MEAT=[pork, chicken, beef], OTHER=[season fruit, rice, pizza, french fries], FISH=[salmon, prawns]}
//6、分组后更该返回映射:OTHER=[普通, 低热量], FISH=[普通, 低热量], MEAT=[普通, 低热量, 高热量]}

        这里要注意一下,普通的单参数 groupingBy(f)(其中 f 是分类函数)实际上是 groupingBy(f,toList()) 的简便写法。

3、分区

        分区是分组的特殊情况,由一个谓词(返回一个布尔值的函数)作为分类函数,它称分区函数。分区函数返回一个布尔值,这意味着得到的分组 Map 的键类型是 Boolean,于是它最多可以分为两组——true是一组,false是一组。

//1、分区
Map<Boolean, List<Dish>> partitioningBy = menu.stream().collect(Collectors.partitioningBy(Dish::isVegetarian));//2、分区后分组
Map<Boolean, Map<Type, List<Dish>>> collect = menu.stream().collect(Collectors.partitioningBy(Dish::isVegetarian, Collectors.groupingBy(Dish::getType)));//1、分区:{false=[pork, beef, chicken, prawns, salmon], true=[french fries, rice, season fruit, pizza]}
//2、分区后分组:{false={FISH=[prawns, salmon], MEAT=[pork, beef, chicken]}, true={OTHER=[french fries, rice, season fruit, pizza]}}

        //分区即是一种特殊的分组,也可以看成是一个保留过滤元素的过滤操作

4、理解收集器接口

        Collector 接口包含了一系列方法,为实现具体的归约操作(即收集器)提供了范本。Collector 接口中实现的许多收集器,例如 toList groupingBy,这也意味着我们可以为 Collector 接口提供自己的实现,从而自由地创建自定义归约操作。

        首先看看 Collector 接口的定义,它列出了接口的签名以及声明的五个方法。

public interface Collector<T, A, R> {Supplier<A> supplier();BiConsumer<A, T> accumulator();BinaryOperator<A> combiner();Function<A, R> finisher();Set<Characteristics> characteristics();
}
  • T 是流中要收集的项目的泛型。
  • A 是累加器的类型,累加器是在收集过程中用于累积部分结果的对象。
  • R 是收集操作得到的对象(通常但并不一定是集合)的类型。

        分析 Collector 接口声明的五个方法,你会发现前四个方法都会返回一个会被 collect 方法调用的函数,而第五个方法 characteristics 则提供了一系列特征,也就是一个提示列表,告诉 collect 方法在执行归约操作的时候可以应用哪些优化。//四个操作函数 + 一个特征值列表

        (1)建立新的结果容器:supplier 方法

        Supplier 方法在调用时它会创建一个空的累加器实例,供数据收集过程使用。在对空流执行操作的时候,这个空的累加器也代表了收集过程的结果//所以lambda收集后不会出现Null的情况

    public Supplier<List<T>> supplier() {return () -> new ArrayList<T>(); //提供一个集合作为累加器实例}

        (2)将元素添加到结果容器:accumulator 方法

        accumulator 方法会返回执行归约操作的函数。该函数将返回 void,因为累加器是原位更新,即函数的执行改变了它的内部状态以体现遍历的元素的效果。比如,把当前元素添加至已经遍历过的元素的列表://定义如何把流中的元素放到累加器中

    public BiConsumer<List<T>, T> accumulator() {return (list, element) -> list.add(element); //定义归约操作}

        (3)对结果容器应用最终转换:finisher方法

        在遍历完流后,finisher 方法必须返回在累积过程的最后要调用的一个函数,以便将累加器对象转换为整个集合操作的最终结果。如果累加器对象恰好与预期的最终结果符合,就无需进行转换。//累加器的对象类型为T,期待返回的最终对象类型也为T,就无需转换 

    public Function<List<T>, List<T>> finisher() {return i -> i;  //无转换,直接返回}

        使用以上三个方法就已经足以对流进行顺序归约,这个逻辑流程如下所示:

        但是为什么还需要另外两个方法呢?答案是并行

        (4)合并两个结果容器:combiner方法

        combiner 方法会返回一个供归约操作使用的函数,它定义了对流的各个子部分进行并行处理时,各个子部分归约所得的累加器要如何合并//并行时,对子流进行合并处理

    public BinaryOperator<List<T>> combiner() {return (list1, list2) -> {list1.addAll(list2);return list1;};}

        有了 combiner 方法,就可以对流进行并行归约了。这个流程就像是这样:

        (5)特征值列表:characteristics 方法

        characteristics 会返回一个不可变的 characteristics 集合,它定义了收集器的行为——尤其是关于流是否可以并行归约,以及可以使用哪些优化的提示。Characteristics 是一个包含三个特征值的枚举。

  • UNORDERED——归约结果不受流中项目的遍历和累积顺序的影响。//无序
  • CONCURRENT——accumulator 函数可以从多个线程同时调用,且该收集器可以并行归约流。如果收集器没有标为 UNORDERED,那它仅在用于无序数据源时才可以并行归约。//并行
  • IDENTITY_FINISH——这表明完成器方法返回的函数是一个恒等函数,可以跳过。这种情况下,累加器对象将会直接用作归约过程的最终结果。
    public Set<Characteristics> characteristics() {return Collections.unmodifiableSet(EnumSet.of(IDENTITY_FINISH, CONCURRENT));}

        至此,收集器的 5 个方法我们都分析完了,然后把上述分析过程的代码组装在一起,就成了一个我们自己定义的 ToList 收集器了,完整的代码如下所示:

import java.util.ArrayList;
import java.util.Collections;
import java.util.EnumSet;
import java.util.List;
import java.util.Set;
import java.util.function.BiConsumer;
import java.util.function.BinaryOperator;
import java.util.function.Function;
import java.util.function.Supplier;
import java.util.stream.Collector;import static java.util.stream.Collector.Characteristics.CONCURRENT;
import static java.util.stream.Collector.Characteristics.IDENTITY_FINISH;public class ToListCollector<T> implements Collector<T, List<T>, List<T>> {@Overridepublic Supplier<List<T>> supplier() {return () -> new ArrayList<T>();}@Overridepublic BiConsumer<List<T>, T> accumulator() {return (list, item) -> list.add(item);}@Overridepublic Function<List<T>, List<T>> finisher() {return i -> i;}@Overridepublic BinaryOperator<List<T>> combiner() {return (list1, list2) -> {list1.addAll(list2);return list1;};}@Overridepublic Set<Characteristics> characteristics() {return Collections.unmodifiableSet(EnumSet.of(IDENTITY_FINISH, CONCURRENT));}
}

        接下来,我们使用Java提供和收集器和自己写的收集器测验一下:

    public static void main(String[] args) {List<String> collect0 = menu.stream().map(Dish::getName).collect(Collectors.toList());List<String> collect1 = menu.stream().map(Dish::getName).collect(new ToListCollector<>());}

        注意,这里我们需要使用 new 来实例化我们自定义的收集器。对比执行的结果,你会发现两个函数的结果都是一样的,这是因为我们实现的 ToList 收集器,大致上就是 Java 实现ToList 收集器的逻辑,下边是  Java 实现ToList 收集器的源码:

    public static <T> Collector<T, ?, List<T>> toList() {return new CollectorImpl<>((Supplier<List<T>>) ArrayList::new,  //供应源List::add,                          //累加器(left, right) -> { left.addAll(right); return left; }, //组合器CH_ID);                              //特征值列表}

        // 上边貌似少了一个 finisher方法,其实这个方法在 CollectorImpl 中给了默认实现,请跟源码

        最后,对于 IDENTITY_FINISH 的收集操作,还有一种方法可以得到同样的结果而无需从头实现新的 Collectors 接口。Stream 有一个重载的 collect 方法可以接受另外三个函数:supplier、accumulator 和 combiner,其语义和 Collector 接口的相应方法返回的函数完全相同。那么上边的收集操作同样可以写成如下形式:

ArrayList<String> collect2 = menu.stream().map(Dish::getName).collect(ArrayList::new, //供应源List::add,      //累加器List::addAll);  //组合器

        第二种形式虽然比前一个写法更为紧凑和简洁,却不那么易读。另外值得注意的是,第二种形式中的 collect 方法不能传递任何 characteristics,所以它永远都是一个 IDENTITY_FINISH CONCURRENT 但并非 UNORDERED 的收集器。

        至此,全文结束。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.hqwc.cn/news/297486.html

如若内容造成侵权/违法违规/事实不符,请联系编程知识网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

2023航天推进理论基础考试划重点(W老师)-液体火箭发动机1

适用于期末周求生欲满满的西北工业大学学生。 1、液体火箭发动机的基本组成及功能是什么&#xff1f; 推力室组件、推进剂供应系统、阀门与调节器、发动机总装元件等组成。 2、液体火箭发动机的分类和应用是什么&#xff1f;3、液体火箭发动机系统、分系统的概念是什么&…

微前端——无界wujie

B站课程视频 课程视频 课程课件笔记&#xff1a; 1.微前端 2.无界 现有的微前端框架&#xff1a;iframe、qiankun、Micro-app&#xff08;京东&#xff09;、EMP&#xff08;百度&#xff09;、无届 前置 初始化 新建一个文件夹 1.通过npm i typescript -g安装ts 2.然后可…

Unity-Shader-渲染队列

Unity-Shader-渲染队列 渲染简介Unity中的几种渲染队列Background (1000)最早被渲染的物体的队列。Geometry (2000) 不透明物体的渲染队列。大多数物体都应该使用该队列进行渲染&#xff0c;也就是Unity Shader中默认的渲染队列。AlphaTest (2450) 有透明通道&#xff0c;需要进…

社会人士可以考英语四六级吗?怎么考四六级

目录 一、社会人士能考英语四六级吗二、社会人士可以参加哪些英语等级考试第一.考个商务英语类证书第二.社会上比较认可的还有翻译证书第三.出国常用的英语凭证第四.职称英语.第五.PETS. 大学英语四六级是为提高我国大学英语课程的教学质量服务。那么社会人士能不能报考英语四六…

【Python从入门到进阶】45、Scrapy框架核心组件介绍

接上篇《44、Scrapy的基本介绍和安装》 上一篇我们学习了Scrapy框架的基础介绍以及环境的搭建&#xff0c;本篇我们来学习一下Scrapy框架的核心组件的使用。 下面的核心组件的介绍&#xff0c;仍是基于这幅图的机制&#xff0c;大家可以再回顾一下&#xff1a; 注&#xff1a;…

设计模式 建造者模式 与 Spring Bean建造者 BeanDefinitionBuilder 源码与应用

建造者模式 定义: 将一个复杂对象的构建与它的表示分离&#xff0c;使得同样的构建过程可以创建不同的表示主要作用: 在用户不知道对象的建造过程和细节的情况下就可以直接创建复杂的对象如何使用: 用户只需要给出指定复杂对象的类型和内容, 建造者模式负责按顺序创建复杂对象…

Spring Cloud + Vue前后端分离-第7章 核心业务功能开发

Spring Cloud Vue前后端分离-第7章 核心业务功能开发 7-1 课程管理功能开发 课程管理页面美化 1.课程管理页面美化 demo-course.jpg 复制search.html中的部分代码 course.vue 看效果 测试一下新增修改删除效果 1.课程管理页面美化2 scoped:style下的样式只应用于当前组件…

BUG记录 | 使用阿里云OSS实现文件上传后,得到的url无法在浏览器中打开

项目背景 SpringBoot的项目&#xff0c;使用阿里云对象存储OSS对项目中的文件进行存储&#xff0c;所需文件也会通过IDEA中由官方Demo改编而成的工具类作为接口&#xff0c;调用接口后上传 问题描述 使用阿里云OSS实现文件上传后&#xff0c;通过postman测试得到的url无法在…

MyBatis动态sql中foreach标签介绍和使用

MyBatis动态sql中foreach标签介绍和使用 参数解释&#xff1a; foreach 的主要作用在构建 in 条件中&#xff0c;它可以在 sql 语句中进行迭代一个集合。foreach 元素的属性主要有 collection&#xff0c;item&#xff0c;separator&#xff0c;index&#xff0c;open&#x…

怎么做好数字化工厂的建设?

怎样建设好的数字化工厂&#xff0c;不但须要有充足的费用预算&#xff0c;更加需要科学研究的计划和设计方案&#xff0c;一般做好智能化基本建设&#xff0c;务必要根据下列流程&#xff1a; 一.信息管理系统的计划和设计方案   许多的工厂会购买许多的单独的信息管理系统&…

CreateProcess error=216, 该版本的 %1 与你运行的 Windows 版本不兼容。请查看计算机的系统信息,然后联系软件发布者。

第一个go程序就出错了&#xff0c;错误提示&#xff1a; Error running ‘go build hello.go’: Cannot run program “C:\Users\Administrator\AppData\Local\Temp___go_build_hello_go.exe” (in directory “G:\go\workspace”): CreateProcess error216, 该版本的 %1 与你运…

【面向对象】对比JavaScript、Go、Ada、Python、C++、Java、PHP的访问限制。

在不同编程语言中&#xff0c;控制成员&#xff08;变量、方法、类等&#xff09;可见性的机制不尽相同。以下是对比JavaScript、Go、Ada、Python、C、Java、PHP所使用的访问限制关键字和约定&#xff1a; 一、JavaScript ### JavaScript访问限制 早期的JavaScript并没有类似…