继之前了解了StreamAPI的创建以及中间操作后,今天主要说一下StreamAPI的最后一步,也就是终止操作。
终止操作主要有查找与匹配,以及归约与收集,这些操作很像我们平时的sql操作,也比较好理解。
首先看看查找与匹配,有这样几种操作:
allMatch-检查是否匹配所有元素
anyMatch-检查是否至少匹配一个元素
noneMatch-检查是否没有匹配所有元素
findFirst-返回第一个找到的元素
findAny-返回任意一个找到的元素
count-返回流中的元素个数
max-返回流中的最大值
min-返回流中的最小值
接下来我们逐一看看他们的用法:
首先是allMatch,比如我们有这样一个学生集合:
List<Student> stus= Arrays.asList(
new Student("张三",16,168.25, Student.Status.FREE),
new Student("李四",48,178.25,Student.Status.VOCATION),
new Student("王五",12,148.45,Student.Status.BUSY),
new Student("赵六",18,180.68,Student.Status.FREE),
new Student("田七",18,180.68,Student.Status.VOCATION)
);
我们想知道该集合中的每个学生是不是状态都为BUSY,先看allMatch方法源码:
这是一个参数为断言型接口返回值为布尔值的方法,说明我们使用该方法会得到true或false两种结果:
接下来完成我们的功能:
boolean b1 = stus.stream().allMatch((e)->e.getStatus().equals(Student.Status.BUSY));
System.out.println(b1);
很明显,结果会是false
类似的,anyMatch方法也是这样使用,但因为它是用于检查是否有一个学生状态为BUSY,所以运行结果会是true
boolean b2=stus.stream().anyMatch(e->e.getStatus().equals(Student.Status.BUSY));
System.out.println(b2);
接下来是noneMatch方法,检查是否全部状态不为BUSY,结果为false
boolean b3=stus.stream().noneMatch(e->e.getStatus().equals(Student.Status.BUSY));
System.out.println(b3);
接下来是两个比较特殊的方法findFirst和findAny,由于两者使用类似,我们放在一起,我们以findFirst为例,先查看该方法的源码:
这是一个无参数但返回值为Optional对象的方法,Optional对象是JAVA8的又一新特性,它用于帮助我们解决泛型中的对象是否为空,避免空指针异常的出现,这里我们往后再提,既然需要这样一个类型,那么我们就用这样一个类型进行接收,假设我们需要找到集合中身高最高的学生,我们可以先按照身高进行排序,再使用findFirst方法取到第一个学生,即为身高最高的学生:
Optional<Student> optional=stus.stream().sorted((e1, e2)->-Double.compare(e1.getHeight(),e2.getHeight())).findFirst();
System.out.println(optional.get());
注意在比较时带上-号,这样才是从大到小进行排序,得到满足条件的学生后,我们使用Optional类中的get方法取出该Student对象,并进行打印,结果如下:
类似地,我们再使用findAny方法,稍微修改我们的功能,假如我们需要找到任意一个状态为FREE的学生,我们可以先使用filter方法过滤出所有满足该条件的学生,再进行提取:
Optional<Student> op=stus.parallelStream().filter(e->e.getStatus().equals(Student.Status.FREE)).findAny();
System.out.println(op.get());
由于该方法是随机取,我们多运行几次看效果:
集合中有两名学生状态都为FREE,所以我们运行多次,就可以看到两者的信息
接下来是三个常用的方法,计数,最大值最小值,我们放在一个方法里一起运行:
@Test
public void countMaxMin(){
long count=stus.stream().count();
System.out.println(count);
Optional<Student> op1=stus.stream().max((e1,e2)->Double.compare(e1.getHeight(),e2.getHeight()));
System.out.println(op1.get());
Optional<Double> op2=stus.stream().map(Student::getHeight).min(Double::compareTo);
System.out.println(op2.get());
}
值得注意的是count方法返回值为long型,而max以及min方法返回值为Optional对象,max与min方法类似,参数都是为comparator接口,这与我们的自定义排序sorted方法类似:
方法中分别统计了流中共有多少个元素,找出身高最高的同学以及找出这批同学最小身高
使用max方法,我们可以较上面的findFirst方法更轻松的得到身高最高的那位同学,运行结果如下:
接下来是归约与收集
归约,可以将流中的元素反复结合起来得到一个值,用通俗的话讲,可以使用它对流中的元素进行一些计算,关键字为reduce,而它有两种使用方法,先说说第一种:
reduce(T identity,BinaryOperator)
其中T代表起始值,而BinaryOperator是一个函数型接口,它是BiFunction<T,T,T>的一个子接口,这里我们以求一个集合中的数字总和为例,使用该方法:
List<Integer> list=Arrays.asList(1,2,3,4,5,6,7,8,9,10);
Integer sum=list.stream().reduce(0,(x,y)->x+y);
System.out.println(sum);
该方法的原理我们可以理解为一个递归运算,即起始值为0,每一次执行求和之后,下一次的起始值会是上一次的和。
然后是第二种使用方法:
reduce(BinaryOperator)
只有一个BinaryOperator接口作为参数,比如我们求上面的学生集合的身高总和(好像该需求没有什么实际用处哈哈哈),先上代码:
Optional<Double> optional=stus.stream().map(Student::getHeight).reduce(Double::sum);//相比两个参数的方法,因为没有起始值,所以可能为空
System.out.println(optional.get());
我们先使用map方法获取每个学生的身高,再进行求和
值得注意的是,因为该方法只有一个参数而没有给定初始值,所以在我们使用getHeight方法时,也许会出现该属性为空,而导致后续的一系列操作报错,也就是有可能会导致空指针异常,所以该方法的返回值要求是一个Optional对象,而这种map-reduce的方法也是一种常用的编程模式,即map-reduce模式,它起源于谷歌的几篇学术论文,也是现在谷歌网络搜索的一个模式。
接下来说一说收集,它可用于将流转化为其他形式,用于给流中的元素汇总,具体它能做什么,我们看代码:
先看看收集collect方法:
这是一个参数为Collector接口的方法,collector接口中包含了许多对流的收集方法,这里就不一一列出,但有一个名为Collectors的类提供了许多静态方法更能方便我们创建一些收集器实例,比如我们要收集student集合中所有学生的名字,那么肯定是使用一个String集合进行接收,我们可以这样写:
List<String> list=stus.stream().map(Student::getName).collect(Collectors.toList());
list.forEach(System.out::println);
先使用map将getName方法映射到流中每个元素形成一个带名字的流,再使用collect方法,调用Collectors类中的toList方法将流转化成list集合,最后输出打印,同样的我们还可以用该类中的toset()方法将流转化为Set集合,或者也可以使用toCollection()方法,在括号中直接new我们想要的集合类型,比如我们想用Hashset进行接收:
HashSet<String> hashSet=stus.stream().map(Student::getName).collect(Collectors.toCollection(HashSet::new));
hashSet.forEach(System.out::println);
当然Collectors类还有许多我们常用的收集操作:
统计:
Long count = stus.stream().collect(Collectors.counting());
System.out.println(count);
求平均值(这里求的是student集合中学生的身高平均值):
Double avg=stus.stream().collect(Collectors.averagingDouble(Student::getHeight));
System.out.println(avg);
求和:
Double sum=stus.stream().collect(Collectors.summingDouble(Student::getHeight));
System.out.println(sum);
最大值以及最小值:
Optional<Student> optional=stus.stream().collect(Collectors.maxBy((e1,e2)->Double.compare(e1.getHeight(),e2.getHeight())));
System.out.println(optional.get());
System.out.println("============================");
Optional<Double> optional2 = stus.stream().map(Student::getHeight).collect(Collectors.minBy(Double::compare));
System.out.println(optional2.get());
与之前的最大值最小值用法类似
当然还有分组以及分区操作,这类似于我们在sql中分组查询,比如我们现在将student集合按年龄进行分组,年龄小于等于35岁为青年,35值50之间为中年,50以上为老年,我们先写下代码:
stus.stream().collect(Collectors.groupingBy((e) ->{
if (e.getAge()<=35){
return "青年";
}else if (e.getAge()<=50){
return "中年";
}else {
return "老年";
}
}));
不难看出,这里我们需要用Map集合进行接收,键为我们判断的类型,而值则为满足条件的对象:
Map<String,List<Student>> map=stus.stream().collect(Collectors.groupingBy((e) ->{
if (e.getAge()<=35){
return "青年";
}else if (e.getAge()<=50){
return "中年";
}else {
return "老年";
}
}));
System.out.println(map);
运行结果如下:
{青年=[Student{id=null, name=‘张三’, age=16, height=168.25, status=FREE}, Student{id=null, name=‘田七’, age=18, height=180.68, status=VOCATION}], 老年=[Student{id=null, name=‘赵六’, age=56, height=180.68, status=FREE}], 中年=[Student{id=null, name=‘李四’, age=48, height=178.25, status=VOCATION}, Student{id=null, name=‘王五’, age=36, height=148.45, status=BUSY}]}
当然,我们还可以像sql中一样,多条件分组,比如我们以三种状态进行分组,再在每种状态分组中以年龄段进行一次分组,那么我们可以这样写:
stus.stream().collect(Collectors.groupingBy(Student::getStatus,Collectors.groupingBy((e)->{
if (e.getAge()<=35){
return "青年";
}else if (e.getAge()<=50){
return "中年";
}else {
return "老年";
}
})));
即在分组中再追加其他分组条件,而这里用于接收的Map类型比较复杂,我们逐一分析,首先最大的分组是按状态分组,所以键为状态,而第二次分组按照年龄段进行分组得出的结果是一个键为年龄段值为Student对象的Map集合,所以最后我们接收的Map集合应该是:
Map<Student.Status,Map<String,List<Student>>> map2=stus.stream().collect(Collectors.groupingBy(Student::getStatus,Collectors.groupingBy((e)->{
if (e.getAge()<=35){
return "青年";
}else if (e.getAge()<=50){
return "中年";
}else {
return "老年";
}
})));
System.out.println(map2);
最后运行结果如下:
{BUSY={中年=[Student{id=null, name=‘王五’, age=36, height=148.45, status=BUSY}]}, FREE={青年=[Student{id=null, name=‘张三’, age=16, height=168.25, status=FREE}], 老年=[Student{id=null, name=‘赵六’, age=56, height=180.68, status=FREE}]}, VOCATION={青年=[Student{id=null, name=‘田七’, age=18, height=180.68, status=VOCATION}], 中年=[Student{id=null, name=‘李四’, age=48, height=178.25, status=VOCATION}]}}
类似的,还有分区:
比如我们需要分别收集是否满足某个条件的信息,如收集Student集合中是否满足身高大于160的学生:
那么我们将区域可以划分为满足或不满足,即true或false,所以用于接收的Map集合可以写成:
Map<Boolean,List<Student>> map= stus.stream().collect(Collectors.partitioningBy((e) ->e.getHeight()>160));
System.out.println(map);
当然,collector类还有其他一些比较好用的静态方法,比如我们上面总是用到的几种计算,像平均值,最大值最小值等等,JAVA8中使用了一个类将这些常用的数字封装在一起,并且在Collectors类中也有对应的方法能够一次全部求出:
DoubleSummaryStatistics statistics=stus.stream().collect(Collectors.summarizingDouble(Student::getHeight));
System.out.println(statistics);
运行结果:
还有一个拼接的操作,比如我们需要拼接一段字符串,这里我们拼接student集合中每位同学的姓名,可以这样写:
String string = stus.stream().map(Student::getName).collect(Collectors.joining());
System.out.println(string);
运行结果:
而假如我们想对拼接后的字符串进行一些处理,比如我们想定义好每个字符间的间隔符,在joining方法中加上“,”
String string = stus.stream().map(Student::getName).collect(Collectors.joining(","));
System.out.println(string);
也可以对拼接的字符串首尾进行操作,比如我们在前后加入“—”
String string = stus.stream().map(Student::getName).collect(Collectors.joining(",","---","---"));
System.out.println(string);
总而言之,Collectors类中还有许多方便的收集方法供我们去使用