2

前言:

在前面几篇文章中主要介绍了stream中的筛选、求和、排序以及生成stream的方式,今天还要讲一个很重要的功能,那就是分组和分区。分组分区实际上就是在筛选的基础上把整个流里面的数据分为不同的组。

测试数据:

1.创建个Student类,里面分别有年级、年龄、科目、名字和科目成绩字段。

public class Student{
    //学生年级
    private String grade;
    //学生年龄
    private Integer age;
    //参加的科目
    private String subject;
    //名字
    private String name;
    //科目成绩
    private Double mark;

    public Student(String grade, Integer age, String subject, String name, Double mark) {
        this.grade = grade;
        this.age = age;
        this.subject = subject;
        this.name = name;
        this.mark = mark;
    }

    public String getGrade() {
        return grade;
    }

    public void setGrade(String grade) {
        this.grade = grade;
    }

    public Integer getAge() {
        return age;
    }

    public void setAge(Integer age) {
        this.age = age;
    }

    public String getSubject() {
        return subject;
    }

    public void setSubject(String subject) {
        this.subject = subject;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public Double getMark() {
        return mark;
    }

    public void setMark(Double rank) {
        this.mark = rank;
    }

    @Override
    public String toString() {
        return "Student{" +
                "grade='" + grade + '\'' +
                ", age=" + age +
                ", subject='" + subject + '\'' +
                ", name='" + name + '\'' +
                ", mark='" + mark + '\'' +
                '}';
    }

2.生成一些测试数据,代表了不同年级不同的学生在不同的学科的成绩

Student student1 = new Student("six",12,"Math","Tim",86D);
Student student2 = new Student("six",12,"English","Tim",84D);
Student student3 = new Student("six",12,"Chinese","Tim",83D);
Student student1s = new Student("six",12,"Math","Rick",84D);
Student student2s = new Student("six",12,"English","Rick",95D);
Student student3s = new Student("six",12,"Chinese","Rick",83D);
students.add(student1);
students.add(student1s);
students.add(student2);
students.add(student2s);
students.add(student3);
students.add(student3s);

Student student4 = new Student("five",11,"Math","Jack",78D);
Student student5 = new Student("five",11,"English","Jack",97D);
Student student6 = new Student("five",11,"Chinese","Jack",68D);
Student student4s = new Student("five",11,"Math","Sam",67D);
Student student5s = new Student("five",11,"English","Sam",93D);
Student student6s = new Student("five",11,"Chinese","Sam",92D);
students.add(student4);
students.add(student4s);
students.add(student5);
students.add(student5s);
students.add(student6);
students.add(student6s);

分组:

1.当我们想要将流中数据分组时只需要调用Collectors.groupingBy()方法即可,这个方法会根据我们传递进来的字段进行分组,比如下面这个例子是按学生的名字分组的,只需要传递Studen::getName方法引用就可以了。最后回返回一个Mapkey值就是名字,value是一个List集合,存放所有名字等于key的对象,这里为了方便阅读进行了一下格式化,没有直接打印map

//把学生按名字分组
public static void groupByName(){
    Map<String,List<Student>> map = students.stream().collect(Collectors.groupingBy(Student::getName));
    Set ketSet = map.keySet();
    for (Object key:ketSet){
        System.out.println(map.get(key));
        System.out.println("==============");
    }
}

运行结果:

[Student{grade='six', age=12, subject='Math', name='Rick', mark='84.0'}, Student{grade='six', age=12, subject='English', name='Rick', mark='95.0'}, Student{grade='six', age=12, subject='Chinese', name='Rick', mark='83.0'}]
==============
[Student{grade='six', age=12, subject='Math', name='Tim', mark='86.0'}, Student{grade='six', age=12, subject='English', name='Tim', mark='84.0'}, Student{grade='six', age=12, subject='Chinese', name='Tim', mark='83.0'}]
==============
[Student{grade='five', age=11, subject='Math', name='Jack', mark='78.0'}, Student{grade='five', age=11, subject='English', name='Jack', mark='97.0'}, Student{grade='five', age=11, subject='Chinese', name='Jack', mark='68.0'}]
==============
[Student{grade='five', age=11, subject='Math', name='Sam', mark='67.0'}, Student{grade='five', age=11, subject='English', name='Sam', mark='93.0'}, Student{grade='five', age=11, subject='Chinese', name='Sam', mark='92.0'}]

2.上面那个例子是直接按成绩分组,但是有时候需要对数据按多个属性分组,比如想要将同个科目下同样成绩的对象分为一类,只需要在原来分组的基础上再进行分组,调用Collectors.groupingBy()的另一个重载方法,第二个参数再传递一个分组操作即可,要注意的是因为我们进行了二次分组所以返回的Map也多嵌套了一层,最外层的key是一层分组的成绩,里面的key则是科目的名称,value则是名称相同的对象集合。可以看到运行结果最外一层把成绩相同的放一起了,里面那层则把名字相同的又放一起了。

//把学生按科目成绩分组
public static void groupByMarkAndSubject(){
    Map<Double,Map<String,List<Student>>> map =
               students.stream().collect(Collectors.groupingBy(Student::getMark,Collectors.groupingBy(Student::getSubject)));
    Set ketSet = map.keySet();
    for (Object key:ketSet){
        System.out.println(map.get(key));
        System.out.println("==============");
    }
}

运行结果:

{Math=[Student{grade='five', age=11, subject='Math', name='Sam', mark='67.0'}]}
==============
{Chinese=[Student{grade='five', age=11, subject='Chinese', name='Jack', mark='68.0'}]}
==============
{Math=[Student{grade='five', age=11, subject='Math', name='Jack', mark='78.0'}]}
==============
{Chinese=[Student{grade='six', age=12, subject='Chinese', name='Tim', mark='83.0'}, Student{grade='six', age=12, subject='Chinese', name='Rick', mark='83.0'}]}
==============
{English=[Student{grade='six', age=12, subject='English', name='Tim', mark='84.0'}], Math=[Student{grade='six', age=12, subject='Math', name='Rick', mark='84.0'}]}
==============
{Math=[Student{grade='six', age=12, subject='Math', name='Tim', mark='86.0'}]}
==============
{Chinese=[Student{grade='five', age=11, subject='Chinese', name='Sam', mark='92.0'}]}
==============
{English=[Student{grade='five', age=11, subject='English', name='Sam', mark='93.0'}]}
==============
{English=[Student{grade='six', age=12, subject='English', name='Rick', mark='95.0'}]}
==============
{English=[Student{grade='five', age=11, subject='English', name='Jack', mark='97.0'}]}

3.上面两个例子的分组产生的Mapkey值都是直接取的对象里的值,其实也可以自己规定分组的规则和key的名字,我们可以在Lambda的右边使用大括号,在大括号里面写逻辑代码,只要保证返回的值是符合格式的就可以,比如现在就是以85分为界限进行分组,Mapkey值也也就是我们返回的两个值。

//找出所有成绩在85之上的学生(包括85)分为合格,85之下的为不合格
public static void getMarkByStandard(){
    Map<String,List<Student>> map = students.stream().collect(Collectors.groupingBy(student ->{
        if (student.getMark().intValue() >= 85){
            return "合格";
        }else{
            return "不合格";
        }
    }));

    System.out.println(map.get("合格"));
    System.out.println(map.get("不合格"));
    }

运行结果:

[Student{grade='six', age=12, subject='Math', name='Tim', mark='86.0'}, Student{grade='six', age=12, subject='English', name='Rick', mark='95.0'}, Student{grade='five', age=11, subject='English', name='Jack', mark='97.0'}, Student{grade='five', age=11, subject='English', name='Sam', mark='93.0'}, Student{grade='five', age=11, subject='Chinese', name='Sam', mark='92.0'}]
[Student{grade='six', age=12, subject='Math', name='Rick', mark='84.0'}, Student{grade='six', age=12, subject='English', name='Tim', mark='84.0'}, Student{grade='six', age=12, subject='Chinese', name='Tim', mark='83.0'}, Student{grade='six', age=12, subject='Chinese', name='Rick', mark='83.0'}, Student{grade='five', age=11, subject='Math', name='Jack', mark='78.0'}, Student{grade='five', age=11, subject='Math', name='Sam', mark='67.0'}, Student{grade='five', age=11, subject='Chinese', name='Jack', mark='68.0'}]

4.当然这种写法也是支持二级分组的,来尝试一下。

//按年级分组的基础下分成合格组和不合格的组 
public static void groupByMarkAndGrade(){
    Map<String,Map<String,List<Student>>> map = students.stream().collect(Collectors.groupingBy(Student::getGrade,
           Collectors.groupingBy(student -> {
                if (student.getMark().intValue() >= 85){
                    return "合格";
                }else{
                    return "不合格";
                }
            })));
    System.out.println(map.get("six"));
    System.out.println(map.get("five"));
}

运行结果:

{不合格=[Student{grade='six', age=12, subject='Math', name='Rick', mark='84.0'}, Student{grade='six', age=12, subject='English', name='Tim', mark='84.0'}, Student{grade='six', age=12, subject='Chinese', name='Tim', mark='83.0'}, Student{grade='six', age=12, subject='Chinese', name='Rick', mark='83.0'}], 合格=[Student{grade='six', age=12, subject='Math', name='Tim', mark='86.0'}, Student{grade='six', age=12, subject='English', name='Rick', mark='95.0'}]}
{合格=[Student{grade='five', age=11, subject='English', name='Jack', mark='97.0'}, Student{grade='five', age=11, subject='English', name='Sam', mark='93.0'}, Student{grade='five', age=11, subject='Chinese', name='Sam', mark='92.0'}], 不合格=[Student{grade='five', age=11, subject='Math', name='Jack', mark='78.0'}, Student{grade='five', age=11, subject='Math', name='Sam', mark='67.0'}, Student{grade='five', age=11, subject='Chinese', name='Jack', mark='68.0'}]}

5.有时候我们只是想要统计每个分组的数量,而不是获得每个分组里面元素的详细信息,这个也是可以实现的。只是在上述的二级分组基础上调用了Collectors.counting()方法来统计每个分组里面的数量

//计算每个年级合格人数和不合格的人数
public static void groupByMarkAndGradeCounts(){
    Map<String,Map<String,Long>> map = students.stream().collect(Collectors.groupingBy(Student::getGrade,
            Collectors.groupingBy(student -> {
                if (student.getMark().intValue() >= 85){
                    return "合格";
                }else{
                     return "不合格";
                }
            },Collectors.counting())
        )
    );
    System.out.println(map);
}

运行结果:

{six={不合格=4, 合格=2}, five={合格=3, 不合格=3}}

6.在分组的基础上获得分数最高的学生的信息

//在分组的基础上获得成绩最高的学生信息
public static void getLargeByMarkAndGrade(){
    Map<String,Optional<Student>> map = students.stream().collect(Collectors.groupingBy(Student::getGrade,
            Collectors.maxBy(Comparator.comparing(Student::getMark)
                )
            ));
    System.out.println(map);
}

运行结果:

{six=Optional[Student{grade='six', age=12, subject='English', name='Rick', mark='95.0'}], five=Optional[Student{grade='five', age=11, subject='English', name='Jack', mark='97.0'}]}

我们注意到返回的map中的对象都包了一层Optional,但是实际上如果分组中没有值的话是不可能返回一个空的Optional的,所以这个Optional根本没什么用处看上去还非常臃肿,其实这个也是可以去掉的。只需要调用Collectors.collectingAndThen()包住本来要进行的分组操作,然后再第二个参数里面调用Optional::get方法引用就可以在返回的Map中去掉包住对象的Optional了。

Map<String,Student> map = students.stream().collect(Collectors.groupingBy(Student::getGrade,
        Collectors.collectingAndThen(
                Collectors.maxBy(Comparator.comparing(Student::getMark)), Optional::get)
            )
        );

运行结果:

{six=Student{grade='six', age=12, subject='English', name='Rick', mark='95.0'}, five=Student{grade='five', age=11, subject='English', name='Jack', mark='97.0'}}

分区:

分区其实就是特殊的分组,它只是固定将流中的数据分为两组,大致用法其实和分组一样。

public static void partitionByStandard(){
    Map<String,Map<Boolean,Long>> map = students.stream().collect(Collectors.groupingBy(Student::getGrade,
        Collectors.partitioningBy(student -> {
            if (student.getMark().intValue() >= 85){
                return true;
            }else{
                return false;
            }
        },Collectors.counting())
        )
    );
    System.out.println(map);
}

运行结果:

{six={false=4, true=2}, five={false=3, true=3}}

Half
238 声望17 粉丝

The Long Way Round.