java8 Streams API (2)

本节我们将仍然以Stream为例,介绍流的规约操作。

规约操作(reduction operation)又被称作折叠操作(fold),是通过某个连接动作将所有元素汇总成一个结果的过程。

元素求和、求最大值或最小值、求出元素总个数、将所有元素转换成一个列表或集合,都属于规约操作。Stream类库有两个通用的规约操作reduce()和collect(),也有一些为简化书写而设计的专用规约操作,比如sum()max()min()count()等。

最大或最小值这类规约操作很好理解(至少方法语义上是这样),我们着重介绍reduce()collect(),这是比较有魔法的地方。

(1)reduce()

// sum()、max()、min()、count()
List<Integer> integers = new ArrayList<>();
integers.add(1);
integers.add(3);
integers.add(5);
integers.add(7);
// 找出集合中的最大值 Optional是一个值的容器,使用它可以避免null值的麻烦
Optional<Integer> max = integers.stream().max(Comparator.comparingInt(i -> i));
System.out.println("最大值为:" + max.get());

// 找出集合中的最小值
Optional<Integer> min = integers.stream().min((Comparator.comparingInt(o -> o)));
System.out.println("最小值:" + min.get());

// 对集合求和
int sum = integers.stream().reduce(0, Integer::sum, Integer::sum);
// 下面的效果和上面的效果一样,只是写法不同,上面使用的是引用
// int sum = integers.stream().reduce(0, (integer, integer2) -> integer + integer2, (integer, integer2) -> integer + integer2);
// 使用下面这种更加简洁  
Optional<Integer> sum2 = integers.stream().reduce(Integer::sum);
System.out.println("对集合求和:" + sum);
System.out.println("对集合求和:" + sum2.get());

// 求集合中元素个数
int count = (int) integers.stream().count();
// 一般都用下面这个,不用上面的这种方式求集合大小
int count2 = integers.size();
System.out.println("集合元素个数:" + count);

以上都是一些简单的用法,因为复杂的我也没学会,哈哈。


(2)collect()

reduce()擅长的是生成一个值,如果想要从Stream生成一个集合或者Map等复杂的对象该怎么办呢?这时就需要用到collect()收集器。

不夸张的讲,如果你发现某个功能在Stream接口中没找到,十有八九可以通过collect()方法实现。collect()是Stream接口方法中最灵活的一个,学会它才算真正入门Java函数式编程。

将stream流转成会list、set容器或者map集合。这样就可以实现list、set和map之间的转换操作。

Stream<String> stream = Stream.of("I", "love", "liangliang");
// 将stream流转换成list
List<String> list = stream.collect(Collectors.toList());
// 将stream流转换成set
Set<String> set = stream.collect(Collectors.toSet());
// 将stream流转换成map
Map<String, Integer> map3 = stream.collect(Collectors.toMap(Function.identity(), String::length));
System.out.println("map3=" + map3);
  • Function.identity()是干什么的?

    • Java 8允许在接口中加入具体方法。接口中的具体方法有两种,default方法和static方法(都是已经实现了的方法),identity()就是Function接口的一个静态方法;

    • Function.identity()返回一个输出跟输入一样的Lambda表达式对象,等价于形如t -> t形式的Lambda表达式。

    部分源码如下图所示:

  • String::length是什么意思?

    这种语法形式叫做方法引用(method references),是用来替代某些特定形式的Lambda表达式。如果Lambda表达式的全部内容就是调用一个已有的方法,那么可以用方法引用来替代Lambda表达式。

    方法引用可以细分为如下四类:

    方法引用类别 举例
    引用静态方法 Integer::sum
    引用某个对象的方法 list::add
    引用某个类的方法 String::length
    引用构造方法 HashMap::new

  • Collectors是个什么东西?

    收集器(Collector)是为Stream.collect()方法量身打造的工具接口(类)。考虑一下将一个Stream转换成一个容器(或者Map)需要做哪些工作?

    我们至少需要前两样东西:

    1. 目标容器是什么?是ArrayList还是HashSet,或者TreeMap。
    2. 新元素如何添加到容器中?是List.add()还是Map.put()
    3. 如果并行的进行规约,还需要告诉collect() 多个部分结果如何合并成一个。

    结合以上分析,collect()方法定义为<R> R collect(Supplier<R> supplier, BiConsumer<R,? super T> accumulator, BiConsumer<R,R> combiner),三个参数依次对应上述三条分析。不过每次调用collect()都要传入这三个参数太麻烦,因此收集器Collector就是对这三个参数的简单封装。

    所以collect()的另一定义为<R,A> R collect(Collector<? super T,A,R> collector)Collectors工具类可通过静态方法生成各种常用的Collector。

    如下代码所示:

    // 将Stream规约成List
    Stream<String> stream4 = Stream.of("I", "love", "liangliang");
    // 方式1
    List<String> list4 = stream4.collect(ArrayList::new, ArrayList::add, ArrayList::addAll);
    // 方式2 一般情况下都是采用的这种方式
    List<String> list4 = stream4.collect(Collectors.toList());
    System.out.println("list4=" + list4);
    

    通常情况下我们不需要手动指定collect()的三个参数,而是调用collect(Collector<? super T,A,R> collector)方法,并且参数中的Collector对象大都是直接通过Collectors工具类获得。实际上传入的收集器的行为决定了collect()的行为。


(3)使用collect()生成Collection

前面已经提到通过collect()方法将Stream转换成容器的方法,这里再汇总一下。将Stream转换成List或Set是比较常见的操作,所以Collectors工具已经为我们提供了对应的收集器,通过如下代码即可完成:

// 将Stream转换成List或Set
Stream<String> stream = Stream.of("I", "love", "liangliang" );
List<String> list = stream.collect(Collectors.toList());
Set<String> set = stream.collect(Collectors.toSet()); 

上述代码能够满足大部分需求,但由于返回结果是接口类型,我们并不知道类库实际选择的容器类型是什么,有时候我们可能会想要人为指定容器的实际类型,这个需求可通过Collectors.toCollection(Supplier<C> collectionFactory)方法完成。

// 使用toCollection()指定规约容器的类型
ArrayList<String> arrayList = stream.collect(Collectors.toCollection(ArrayList::new));
HashSet<String> hashSet = stream.collect(Collectors.toCollection(HashSet::new));

(4)使用collect()生成Map

Stream背后依赖于某种数据源,数据源可以是数组、容器等,但不能是Map。反过来从Stream生成Map是可以的,但我们要想清楚Map的key和value分别代表什么,根本原因是我们要想清楚要干什么。通常在三种情况下collect()的结果会是Map:

  • 使用Collectors.toMap()生成的收集器,用户需要指定如何生成Map的key和value;

  • 使用Collectors.partitioningBy()生成的收集器,对元素进行二分区操作时用到;

  • 使用Collectors.groupingBy()生成的收集器,对元素做group操作时用到。

情况1:使用toMap()生成的收集器,这种情况是最直接的,前面例子中已提到,这是和Collectors.toCollection()并列的方法。如下代码所示:

// appleList是一个Apple类型的list    Apple是一个对象,这里的意思是用Apple对象的id属性作为key,name属性作为value
Map<Integer, String> stringMap = appleList.stream().collect(Collectors.toMap(Apple::getId, Apple::getName));
System.out.println("将list转成map,id为key,name为value:" + stringMap);

/**
* List -> Map
* 需要注意的是:
* toMap 如果集合对象有重复的key,会报错Duplicate key ....
*  apple1,apple12的id都为1。
*  可以用 (k1,k2)->k1 来设置,如果有重复的key,则保留key1,舍弃key2
*/
Map<Integer, Apple> appleMap = appleList.stream().collect(Collectors.toMap(Apple::getId, a -> a, (k1, k2) -> k1));
System.out.println("toMap()方法重复key情况:" + appleMap);

情况2:使用partitioningBy()生成的收集器,这种情况适用于将Stream中的元素依据某个二值逻辑(满足条件,或不满足)分成互补相交的两部分,比如男女性别、成绩及格与否等。下列代码展示将学生分成成绩及格或不及格的两部分。

// 将学生进行分组,成绩大于60算及格
List<Student> studentList = new ArrayList<>();
Student student = new Student("zhangsan", "java", 80);
Student student2 = new Student("lisi", "c", 50);
Student student3 = new Student("zhaoliu", "jvm", 40);
Student student4 = new Student("wanger", "c++", 70);
studentList.add(student);
studentList.add(student2);
studentList.add(student3);
studentList.add(student4);

Map<Boolean, List<Student>> listMap = studentList.stream().collect(Collectors.partitioningBy(student1 -> student1.getScore() >= 60));
System.out.println("学生成绩:" + listMap);
// 输出结果如下
{false=[Student{name='lisi', course='c', score=50}, Student{name='zhaoliu', course='jvm', score=40}], true=[Student{name='zhangsan', course='java', score=80}, Student{name='wanger', course='c++', score=70}]}

// 如果要得到合格的学生的信息 listMap.get(true) 即可

情况3:使用groupingBy()生成的收集器,这是比较灵活的一种情况。跟SQL中的group by语句类似,这里的groupingBy()也是按照某个属性对数据进行分组,属性相同的元素会被对应到Map的同一个key上。如下代码所示:按照id来进行分组,这里只是模拟,实际是不会这样做的。

List<Apple> appleList = new ArrayList<>();

Apple apple1 = new Apple(1, "苹果1", new BigDecimal("3.25"), 10);
Apple apple2 = new Apple(2, "苹果2", new BigDecimal("1.35"), 20);
Apple apple3 = new Apple(3, "香蕉", new BigDecimal("2.89"), 30);
Apple apple4 = new Apple(4, "荔枝", new BigDecimal("9.99"), 40);

appleList.add(apple1);
appleList.add(apple2);
appleList.add(apple3);
appleList.add(apple4);

// list转map进行分组,按照某个属性来进行分组 
Map<Integer, List<Apple>> map = appleList.stream().collect(Collectors.groupingBy(Apple::getId));
System.out.println("按照id来进行分组,id为key,对象为value,打印输出map:" + map);

以上只是分组的最基本用法,有些时候仅仅分组是不够的。在SQL中使用group by是为了协助其他查询,比如1. 先将员工按照部门分组,2. 然后统计每个部门员工的人数。Java类库设计者也考虑到了这种情况,增强版的groupingBy()能够满足这种需求。增强版的groupingBy()允许我们对元素分组之后再执行某种运算,比如求和、计数、平均值、类型转换等。这种先将元素分组的收集器叫做上游收集器,之后执行其他运算的收集器叫做下游收集器(downstream Collector)。

// 使用下游收集器统计每个部门的人数
Map<Department, Integer> totalByDept = employees.stream()
                    .collect(Collectors.groupingBy(Employee::getDepartment,Collectors.counting()));// 下游收集器

上面代码的逻辑是不是越看越像SQL?高度非结构化。还有更狠的,下游收集器还可以包含更下游的收集器,这绝不是为了炫技而增加的把戏,而是实际场景需要。考虑将员工按照部门分组的场景,如果我们想得到每个员工的名字(字符串),而不是一个个Employee对象,可通过如下方式做到:

// 按照部门对员工分布组,并只保留员工的名字
Map<Department, List<String>> byDept = employees.stream()
                .collect(Collectors.groupingBy(Employee::getDepartment,
                                               Collectors.mapping(Employee::getName, // 下游收集器
                                                                  Collectors.toList())));// 更下游的收集器

参考博文:
(1) https://blog.csdn.net/lu930124/article/details/77595585
(2)https://objcoding.com/2019/03/04/lambda/ (非常详细,值得仔细阅读)

原文地址:https://www.cnblogs.com/jasonboren/p/13749534.html