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

2017-08-07 From 程序之心 By 丁仪

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

 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 Liststudents = 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 map = students.stream() .collect(Collectors.toMap(Student::getName, Student::getAge)); 
  System.out.println(map); 
  TreeSetset = 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)]}

本文来源:程序之心,转载请注明出处!

君子曰:学不可以已。
《计算机科学导论(原书第4版)》

本书是国际知名的高等学校计算机科学及相关专业基础课教材,也是非常受欢迎的计算机入门读物。该教材是一本百科全书式的计算机专业入门指南,涉及计算机科学的方方面面。虽然读者对象是计算机专业的学生,但这本书深入浅出、引人入胜,勾画出了计算机科学体系的框架,可以为有志于IT行业的学生奠定计算机科学知识的基础,架设进一步深入专业理论学习的桥梁。

发表感想

© 2016 - 2024 chengxuzhixin.com All Rights Reserved.

浙ICP备2021034854号-1    浙公网安备 33011002016107号