手把手教你使用 Java 8 Stream 收集器

前文《Java 8 | Stream 简介与常用方法》介绍了 collect 方法由 Stream 里的值生成一个列表,实际上这里就是使用了收集器来返回一个 List。收集器是一种通用的、从流中生成复杂值的结构。collect 方法使用收集器来完成复杂的操作,方法定义如下。

<R, A> R collect(Collector<? super T, A, R> collector);

Java 8 标准库提供了一些常用的收集器,由 java.util.stream.Collectors 静态类提供。

先准备数据,定义了一个 Student 类描述学生,包括 age、name、man 等域,实现了 Comparable 接口用于比较年龄大小,覆盖了 toString 方法。本文的实例都基于此数据类型。

public class Student implements Comparable {
    long age;//年龄     String name;//姓名     boolean man;//性别男 
    public Student(long age, String name, boolean isMan) {
        this.age = age;
        this.name = name;
        this.man = isMan;
    }
    public long getAge() {
        return age;
    }
    public String getName() {
        return name;
    }
    public boolean isMan() {
        return man;
    }
    @Override
    public String toString() {
        return "(" + age + ", " + name + ", " 
            + (man ? "man":"woman") + ")";
    }
    @Override
    public int compareTo(Object o) {
        Student student = (Student) o;
        return (int)(age - student.getAge());
    }
}

初始化数据,创建 4 个学生对象。

public List<Student> students = Arrays.asList(
new Student(18, "LiLei",true)
    ,new Student(19, "Jack",true)
    ,new Student(16,"Mei",false)
    ,new Student(17,"Andy",false));

转换成其他集合

Collectors 提供了用于生成 List、Set、Collection、Map 等多种数据类型的收集器,主要如下:

toList :从流中生成 List,List 的类型由 Stream 自动判断;

toSet :从流中生成 Set,Set 的类型由 Stream 自动判断;

toMap:从流中生成 Map,key-value 的类型需要自己指定,有两个重载,一个使用默认的 HashMap,一个可以自己指定 Map 类型;

toCollection:从流中生成 Collection,Collection 的具体类型自己指定。

比如可以使用 toMap 方法从学生列表中提取出名字、年龄的 HashMap,可以使用 toCollection 方法从学生列表中提取出按 TreeSet 类型存储的学生集合(按年龄排序生成树)。

public void convertToCollection(){
    Map<String, Long> map = students.stream()
        .collect(Collectors.toMap(Student::getName, Student::getAge));
    System.out.println(map);
    TreeSet<Student> set = students.stream()
         .collect(Collectors.toCollection(TreeSet::new));
    System.out.println(set);
}

执行结果如下,第一行打印了名字、年龄的 Map 数据,第二行打印了按年龄从小到大的排序。

{Mei=16, Jack=19, LiLei=18, Andy=17}
[(16, Mei, woman), (17, Andy, woman), (18, LiLei, man), (19, Jack, man)]

数值计算

Collectors 类提供了一些数值计算的方法,如:

averagingDouble :计算 double 类型数值的平均值;

averagingInt:计算 int 类型数值的平均值;

averagingLong:计算 long 类型数值的平均值;

maxBy:计算最大值;

minBy:计算最小值;

summingDouble :计算 double 类型数值的和;

summingInt:计算 int 类型数值的和;

summingLong:计算 long 类型数值的和;

counting:统计元素的个数。

下面的代码演示了使用 maxBy 从学生列表中找到年龄最大的学生,然后计算所有学生年龄的平均值、和。

public void convertToValue(){
    Student student = students.stream()
        .collect(Collectors.maxBy(Comparator.comparing(Student::getAge)))
        .get();
    System.out.println(student);
    double age = students.stream()
        .collect(Collectors.averagingLong(Student::getAge));
    System.out.println(age);
    long sum = students.stream()
        .collect(Collectors.summingLong(Student::getAge));
    System.out.println(sum);
}

执行结果如下,年龄最大学生为 Jack,所有学生年龄平均值为 17.5,年龄总和为 70。

(19, Jack, man)
17.5
70

数据分块

Collectors 类提供了一种方法可以把集合分成两部分,这个方法为 partitioningBy。该方法接收一个 Predicate 对象用于判断一个元素应该属于哪个部分,然后根据返回的 true 和 false,把元素放入 Map 中。Map 的 key 只有 true 和 false 两个,value 是元素列表。如下的代码演示了根据性别把学生分为两部分。

public void  partition(){
    Map<Boolean, List<Student>> block = students.stream()
        .collect(
            Collectors.partitioningBy(Student::isMan)
        );
    System.out.println(block);
}

执行结果如下,Map 中 false 对应的列表是女生,true 对应的列表是男生。

{false=[(16, Mei, woman), (17, Andy, woman)], true=[(18, LiLei, man), (19, Jack, man)]}

数据分组

把数据分为两部分并不能解决所有问题,Collectors 类提供了 groupingBy 方法,来提供一个可以按需分组的收集器。groupingBy 方法接收一个 Function 对象来对数据进行分组,分组之后的数据也是存储在 Map 中。如下的代码演示了根据学生的年龄对学生进行分组。

public void group(){
    Map<Long, List<Student>> group = students.stream()
        .collect(Collectors.groupingBy(Student::getAge));
    System.out.println(group);
}

执行结果如下,返回的 Map 的 key 是年龄,value 是学生列表。

{16=[(16, Mei, woman)], 17=[(17, Andy, woman)], 18=[(18, LiLei, man)], 19=[(19, Jack, man)]}

字符串

Collectors 类还提供了字符串操作的收集器,用于从流中生成一个字符串。主要方法是:

joining():从流中拼接一个字符串;

joining(CharSequence):从流中拼接一个字符串,并指定分隔符;

joining(CharSequence,CharSequence,CharSequence):从流中拼接一个字符串,并指定分隔符、前缀、后缀;

如下的代码演示了从学生列表中提取出所有学生的名字,并组成一个字符串,使用逗号分隔。

public void string(){
    String names = students.stream()
        .map(Student::getName)
        .collect(Collectors.joining(","));
    System.out.println(names);
}

执行结果如下。

LiLei,Jack,Mei,Andy

组合收集器

多个收集器也可以组合使用,实现非常复杂的功能。mapping、groupingBy、partitioningBy 方法的第二个参数可以传入一个下游收集器,用于在当前收集器工作完成后调用下一个收集器。

如下的代码在前面按照性别对学生分类的基础上,传入了一个下游收集器,下游收集器是一个 maxBy 收集器。收集器的工作分为两步,第一步按性别分类,第二部找出每个学生列表年龄最大的学生。

public void combination(){
    Map<Boolean, Optional<Student>> group = students.stream()
        .collect(
            Collectors.groupingBy(  Student::isMan
                ,Collectors.maxBy(
                    Comparator.comparing(Student::getAge)
                )
             )
        );
    System.out.println(group);
}

执行结果如下,女生中年龄最大的是 Andy,男生中年龄最大的是 Jack。

{false=Optional[(17, Andy, woman)], true=Optional[(19, Jack, man)]}

每周 3 篇学习笔记或技术总结,内容涉及 Java 进阶、虚拟机、MySQL、NoSQL、分布式计算、开源框架等多个领域。关注作者或微信公众号 后端开发那点事儿 第一时间获取最新内容。

    原文作者:程序之心 丁仪
    原文地址: https://zhuanlan.zhihu.com/p/28332576
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞