Java:Stream:filter / forEach / sorted / 聚合 / group / 延迟执行……

更多
2021年09月02日 22点29分 作者:叶飞 修改

复习:函数式编程 / 回调函数 / 重用……


概览

在Java 8中被引入(类似于但远弱于C#中的Linq),可以通过Lambda完成集合中元素的查询等。
List<Person> adults = people.stream()
		.filter(p -> p.Age > 18)
		.collect(Collectors.toList())
		;

F3源代码演示:

首先,Collection中定义了stream()默认方法,由此,所有集合(Map先转成Set)都可以转换成Stream;

然后,Stream中定义了大量的方法,这些方法接收匿名方法/Lambda表达式做参数;

最后,调用这些方法,就会(不严谨,但先这样粗糙的理解)

  1. 遍历集合元素
  2. 用集合元素作为Lambda参数,运行Lambda表达式,
  3. 根据Lambda运行结果输出相应结果


获得Stream

一般又两种方式:

  • Collection的stream()实例方法。List和Set可以直接调用,Map需要首先转化成Collection
    Stream<Person> result = people.stream();
    Stream<Map.Entry<String, Person>> entries = new HashMap<String, Person>().entrySet().stream();
  • Stream静态方法生成:
    //三个元素形成的Stream
    Stream<Integer> ages = Stream.of(28, 16, 23);  		
    //一个元素的Stream
    Stream<Integer> defaultAge = Stream.of(25);
    //一个空的Stream Stream<Integer> empty = Stream.empty();			
    //Stream还可以进行连接
    Stream<Integer> concated = Stream.concat(ages, empty);
    

另外,Stream的很多方法返回的仍然是Stream对象,这样,就可以形成“方法连缀”,极大的提高了代码的流畅性。


演示准备数据

3个学生2个老师:

Teacher fg = new Teacher();
fg.Name = "飞哥";
Teacher xy = new Teacher();		
xy.Name = "小鱼";

Student atai = new Student();
atai.Age = 16;
atai.Score = 85.5;
atai.Name = "阿泰";
atai.Teachers = new ArrayList<>();
atai.Teachers.add(fg);
atai.Teachers.add(xy);

Student bo = new Student();
bo.Age = 17;
bo.Name = "波仔";
bo.Score = 58;
bo.Teachers = new ArrayList<>();
bo.Teachers.add(fg);

Student lang = new Student();
lang.Age = 17;
lang.Name = "浪仔";
lang.Score = 69;
lang.Teachers = new ArrayList<>();
lang.Teachers.add(xy);

List<Student> students = new ArrayList<>();
students.add(atai);
students.add(bo);
students.add(lang);

Student dsx = new Student();
dsx.Age = 23;
dsx.Score = 88;
dsx.Name = "大师兄";

Student lw = new Student();
lw.Age = 23;
lw.Score = 89;
lw.Name = "炜哥"; 

注意:这里为了演示清晰,我们直接使用了字段,同学们完成作业时还是应该使用getter和setter属性。


filter():过滤

所有能返回bool值的表达式都可以:

  • 基于数据的:大于(>)小于(<)等于(==)  不等于(!=)
  • 基于集合的:包含(contains)、元素数量(size)如何
  • 基于字符串的:以……开头(startsWith),包含(contains)
  • ……

同时还可以使用多个filter连缀:

students.stream()
        .filter(s -> s.Score > 80 && s.Age < 18);
students.stream()
        .filter(s -> s.Score > 80)
        .filter(s -> s.Age < 18);

@猜一猜@:两个filter连缀,和一个filter使用逻辑组合运算,运行时有区别么?


forEach():遍历

students.stream()    //遍历每一个元素,输出到控制台
		.forEach(s -> System.out.println(s.Name));
注意你有可能会看到这种写法(复习:方法引用
students.stream()    //不要不知道这是啥意思,^_^
		.forEach(System.out::println);


map():映射/投影

从原有结果集中取出若干属性重新组合成新的集合。比如拿到学生的姓名:

students.stream().map(s->s.Name).forEach(System.out::println);
比如我们要得到:每个老师上了多少门课,怎么办?
students.stream()
.map(s-> new Map.Entry<String, Integer>() {    //匿名对象
	@Override
	public String getKey() {
		return s.Name;    //学生姓名为:键
	}
	@Override
	public Integer getValue() {
		return s.Teachers.size();    //老师数量为:值
	}
	@Override
	public Integer setValue(Integer value) {
		return null;
	}
} )
//这时候s不再指代Student,而是Map.Entry实例
.forEach(s-> System.out.println(s.getKey() + ":" + s.getValue()));

可能你会觉得使用匿名对象有点别扭,那就可以在collect()的时候进行map:

students.stream()
	.collect(Collectors.toMap(s->s.Name, s->s.Teachers.size()))
//这里的forEach()方法不是Stream的,而是Map的
	.forEach((k,v)->System.out.println(k + ":" + v));


XXXmatch():匹配结果

检查stream中是否有元素满足匹配条件:

//所有的学生年龄小于18:false
System.out.println(students.stream().allMatch(s-> s.Age < 18));
//有学生年龄小于18:true
System.out.println(students.stream().anyMatch(s-> s.Age < 18));
//没有学生年龄小于18:false
System.out.println(students.stream().noneMatch(s-> s.Age < 18));


distinct():过滤重复

如果有重复的元素,只保留一个:
students.stream().distinct().forEach(s->System.out.println(s.Name));

注意是否重复的依据是元素继承/重写的Object.equals()方法。所以如果元素是:

  • 普通对象,比如Student,就比较对象地址/引用
  • 包装对象,比如Integer,就比较对象的值


sorted():排序

对结果集还可以按指定规则进行排序

  • 传一个Comparator参数:指示如何比较的Lambda表达式
    students.stream()
    		.sorted((a,b)->b.Age-a.Age)    //根据年龄进行比较
    	.forEach(s->System.out.println(s.Name + ":" + s.Age));
  • 不传参数:要求元素可以比较(实现Comparable)
    students.stream()
    		.sorted()    //没有参数
    	.forEach(s->System.out.println(s.Name + ":" + s.Age));
    public class Student implements Comparable<Student> {
    	@Override
    	public int compareTo(Student o) {
    		// TODO Auto-generated method stub
    		return this.Age-o.Age;
    	}

排序之后还可以取第一个:findFirst()


skip()和limit():分页

@想一想@:分页的本质是什么?比如说:每页10个元素,取第3页的数据。

是不是就是:跳过(skip)20个,再依次取10(limit)个?Stream中庸skip()和limit()可以非常方便的实现:

students.stream()
	.sorted()
	.skip(20).limit(10);
注意这个sorted(),如果没有它的话,出于性能考虑,skip()是以一种乱序的方法(in the encounter order)跳过若干元素。所以我们通常都是要首先进行排序的。

再归纳一下,每页size个元素,取第index页呢?

.skip((index-1)*size).limit(size)
记住这个公式,以后会经常用到的哟……^_^


聚合运算

取元素个数:count()

System.out.println("count:" + students.stream().count());

最大最小值:和sort()一样,需要提供比较方式(Comparator):

  • max()
    Optional<Student> max = students.stream().max((a,b)-> a.Age-b.Age)
  • min()
    Optional<Student> min = students.stream().min((a,b)-> a.Age-b.Age)

注意返回的是Optional泛型类复习,要拿到值还需要调用orElse().get()方法……

需要首先调用collect()方法,然后传入

  • Collectors.summing<Type>()
    int sum = students.stream().collect(Collectors.summingInt(s->s.Age));
  • 或者:Collectors.summarizing<Type>()
    double sum = students.stream()
    		.collect(Collectors.summarizingInt(s->s.Age))
    		.getAverage();
    //		.getSum();
    summarize不是求和,而是统计,所以得到的是统计信息(<Type>SummaryStatistics)还不是具体的“和”或“平均值”,还需要进一步调用getSum()/getAverage()等方法。
PS:同学们感觉如何?这些API设计得友好,混乱!引以为戒。


reduce():合并?

求和还有一种方式:

System.out.println(
		Stream.of(1, 2, 9, 3, 10) // 生成整数流
		.reduce((a, b) -> { // a和b代表相邻两个运算元素
			System.out.println(a + "+" + b + "=" + (a + b));
			//每一次都是把两个元素相加,以其结果作为下一次运算的第一个元素
			return a + b;   
		}).orElse(0));    //返回的是Optional

很难描述啊,o(╥﹏╥)o,输出的结果是:

1+2=3
3+9=12
12+3=15
15+10=25
25
元素间如何运算,有reduce()的参数指定,可以加也可以减,其他任何运算也都行,只是要包装运算的结果和原来的元素是同一类型即可。

对于示例中的Student集合流,我们可以:

  • map之后再reduce:
    int sum = students.stream()
            .mapToInt(s->s.Age).reduce((a,b)->a+b)
            .getAsInt(); 
  • 构建一个临时Student:
    int sum = students.stream()
    		.reduce((a, b) -> new Student(a.Age + b.Age))
    		.get().Age;  //get()之后返回的是Student


分组(group)

将具有相同属性(比如年龄/授课老师/城市)的元素归为一组。

分组过后的结果默认是Map键值对集合:

  • 键:分组依据
  • 值:默认List集合,代表当前组成员

简单分组

分组需要在collect中完成,传入Collectors.groupingBy

//默认分组结果的值类型为List
Map<Integer, List<Student>> aged = 
		students.stream().collect(
				//按单个成员(Age)分组
				Collectors.groupingBy(s->s.Age));
我们可以用forEach把结果输出出来:
aged.forEach((k,v)->{   //K和V代表什么?
	System.out.println(k + ":");
	for (Student student : v) {
		System.out.println(student.Name);
	}
	System.out.println("--------");
});

还有一种常见的需求:分组过后再进行

聚合运算

可以使用groupingBy的重载,传入一个通过Collectors.summingDouble()获取的Collector

students.stream().collect(
		Collectors.groupingBy(s->s.Age, 
				//对每个小组再进行求职
				Collectors.summingDouble(s->s.Score) ))
.forEach((k,v)->System.out.println(k + ":" + v));

如果需要统计最大最小值等,就需要通过Collectors.summarizingDouble()获取的Collector了:

students.stream().collect(
		Collectors.groupingBy(s->s.Age, 
			//对每个小组再进行统计(statistic)
			Collectors.summarizingDouble(s->s.Score)))
//这时候v代表DoubleSummaryStatistics
.forEach((k,v)->System.out.println(k + ":" + v.getMax()));


复杂分组条件

比如我们要把年龄和成绩都相同的分成一组。

可以想象,问题就在这里:

Collectors.groupingBy(s->???));    //写点啥呢?

Java里面没有C#中的匿名类对象,只有自己声明一个类:

class age2score {
	private int age;
	private double score;
	public age2score(int age, double score) {
		this.age = age;
		this.score = score;
	}


然后,groupingBy的就是这个对象(classifier):
Collectors.groupingBy(s-> new age2score(s.Age, s.Score)));
但是,运行结果让你失望了……

因为进行分组,就要对分组的classifier进行比较,而对象默认的比较方式是通过堆地址进行的,所以上面每次迭代都新new出来的对象一定不会相等。咋办呢?

得为age2score重写equals()和hashCode()方法(复习:Object)。

怎么写?八仙过海各显神通了哟,^_^

@Override
public boolean equals(Object obj) {
	age2score as = (age2score)obj;
	return as.age == this.age && as.score == this.score;
}
@Override
public int hashCode() {
	return (String.valueOf(age) + "-" + String.valueOf(score)).hashCode();
}


延迟执行

按Java的官方文档,Stream的操作被分为两种/两个阶段:

  • 中间态:intermediate,返回另外一个Stream,但本身不遍历集合,比如filter()/sort()/map()……
  • 终结态:terminal,遍历集合返回结果,比如collect()/reduce()/max()/min()/findFirst()……

但实际上更流行、更简单的说法,就是延迟执行(deferred execution),或者懒惰执行(lazy execution),或者惰性加载(lazy load)

我们可以把Stream理解成一个管道(pipeline),本身并不存储查询结果:

  • 中间态的方法调用,只是确定管道的形态(比如滤网啊啥的);
  • 只有出现终结态的方法调用,才开启管道,从集合中抽取/收集(collect)、运算(max/min)
演示:sort()不会调用compareTo()方法,但findFirst()会

作用

  • 在进行多条件查询拼接时提高性能,只在最后才遍历集合进行运算:
    students.stream().filter(s->s.Age<18).sorted().findFirst();
    这在我们过滤/排序等条件动态拼接的时候尤其有用
    Stream<Student> stream = students.stream();
    if (onlyTeenage) {   //用户指示是否只检索未成年人
    	stream = stream.filter(s->s.Age<18);
    }//else nothing
    stream.sorted().findFirst();
  • 保证获得的是即时(up-to-date)数据


作业

见:J&C:集合概述 / 迭代器模式 / ER模型 / 仓储模式




Stream Java8
赞: 0 踩: 0

打赏
已收到打赏的 帮帮币

你的 打赏 非常重要!
为了保证文章的质量,每一篇文章的发布,都已经消耗了作者 1 枚 帮帮币
没有“帮帮币”,作者无法发布新的文章。

全系列阅读
评论 / 0

后台开发


其他:WebForm和WebApi

其他ASP.NET框架,如WebForm、WebApi……

RazorPages(Core)

微软推荐的、最新的、基于Razor页面和.NET core的新一代Web项目开发技术,包括Razor Tag Helper、Model绑定和Validation、Session/Cookie、内置依赖注入等……

MVC(Framework)

过去两年间最流行的、基于.NET Framework和MVC模式的ASP.NET MVC框架,主要用于讲解安全、性能、架构和各种实战功能演示……

C#语法

从入门的变量赋值、分支循环、到面向对象,以及更先进的语言特性,如:泛型、Lambda、Linq、异步方法等…………

Java语法

面向过程的变量赋值、分支循环和函数封装;面向对象的封装、继承和多态;以及更高阶的常用类库(集合/IO/多线程……)、lambda等

Java Web开发

SpringMVC

分层架构和综合实战

J&C

Java和C#共有的语法

全部
关键字



帮助

反馈