Java8 之流操作
流的概念
日常生活中,我们经常接触各种流:水流、人流、车流...,流就是流动的,依次向前。Java8 提供了类似的操作,将数据转换为流,逐个进行操作。Java8的流具有以下特点
单向流动
这是流最基本的特征,流只能从源头流向终点,本身是不可逆的,一旦流中的数据流过当前的节点,就不能追溯到之前数据了。
多环节操作
这有点像工厂的流水线,一件产品在流水线上,经过多个环节,每个环节都获得流中的产品,对其进行操作,再提供给下一个环节。
得到流
Java8 对现有的类进行扩展,可以很方便的将一个List或数组对象,转换为流
List list = new ArrayList();
list.add("111");
list.add("222");
list.add("333");
list.stream();
String[] strs = new String[8];
Arrays.stream(strs);
只要执行上面的代码就可以将对象转为流。
得到流后,我们能对流做什么呢?当然是对流进行各种操作,这就相当于逐个遍历流中的数据,对其进行操作。这些操作包括包括、截断、映射、规约等,而且这些操作通常可以连接起来,完成更加复杂的功能。
过滤流
过滤流可以按照一定的条件,对流中的每个元素进行判断,将所有判断结果为真的元素返回,形成新的流。
List strs = Arrays.asList("apple","banana","orange","cherry");
List strAs = strs.stream().filter(str->str.contains("a")).collect(Collectors.toList());
这段代码先定义了一个List,包含4个单词,之后将其转为流,对流进行过滤,只要包含字母“a”的单词,这样就得到新的流。
这段代码中,需要注意两点,一个是流不需要显式的进行迭代,代码中并没有循环语句,流在内部进行数据迭代,将过程隐藏了。另外,在过滤函数filter后,使用collect来将结果收集起来,形成新的List。这是由于,流的操作分为中间操作和终端操作,中间操作不会执行,只有提供了终端操作流才开始流动起来,执行中间操作和终端操作。
filter是对流进行过滤的操作,它接收一个谓词作为参数,传入的谓词会判断流中的元素是否满足条件,返回boolean值,只有为真的元素才会进入后续的流。
截断流
截断流可以获得流的一部分元素,从头部获取,或是跳过头部的若干元素。
List strs = Arrays.asList("apple","banana","orange","cherry");
List strAs = strs.stream().limit(2).collect(Collectors.toList());
limit是获得流头部的若干元素,这里获得了流开头的2个元素,同样,limit是中间操作,需要通过collect这样的终端操作,启动流的运行,并将其转为结果List。
List strs = Arrays.asList("apple","banana","orange","cherry");
List strAs = strs.stream().skip(2).collect(Collectors.toList());
skip则是跳过若干元素的操作,这里就获得了,跳过开头2个元素后的流中的其他元素,并组成list。
映射
映射是对流中的每个元素执行操作,操作结果是另外一个对象,把一个对象转为另外一个对象的过程,称之为映射。Java 8 对流调用map,实现流的映射。map需要传递一个Function函数式接口,也就可以是一个Lambda或者方法引用。下面看一个例子。
List strs = Arrays.asList("apple","banana","orange","cherry");
List strLens = strs.stream().map(str->str.length()).collect(Collectors.toList());
代码通过map,将字串流映射为表示每个字符串长度的整数流,最后转为List输出。
map在实际使用中,常常用来将对象流,转为对象的某个属性的流。比如将学生对象的流,转为学生名字的流,方便后续的操作,前提是后面的操作不会使用到学生对象的其他属性。
另外,流还提供了一个flatMap方法,用于将嵌套的流扁平化。flatMap解释起来稍微复杂一些,我们先定义两个类
public class Teacher {
private String Name;
private List students;
public Teacher(String name, List students) {
Name = name;
this.students = students;
}
public String getName() {
return Name;
}
public void setName(String name) {
Name = name;
}
public List getStudents() {
return students;
}
public void setStudents(List students) {
this.students = students;
}
}
教师类,包含名字和教的学生列表
public class Student {
private String name;
private int age;
public Student(String name, int age) {
this.name = name;
this.age = age;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
}
学生类,包含名字和年龄
接下来初始化一些数据
public static List initData(){
List teachers = new ArrayList<>();
teachers.add(new Teacher("张老师",Arrays.asList(new Student("学生甲",11),new Student("学生乙",12),new Student("学生丙",10))));
teachers.add(new Teacher("李老师",Arrays.asList(new Student("学生丁",11),new Student("学生甲",12),new Student("学生戊",10))));
teachers.add(new Teacher("王老师",Arrays.asList(new Student("学生乙",11),new Student("学生己",12),new Student("学生庚",10))));
teachers.add(new Teacher("赵老师",Arrays.asList(new Student("学生庚",11),new Student("学生甲",12),new Student("学生辛",10))));
return teachers;
}
这里为每个老师分配三名学生,名字互相有重叠。
现在想要得到所有老师教授的学生姓名的列表,该如何操作呢
List students = teachers.stream().map(teacher -> teacher.getStudents().stream().map(Student::getName)).collect(Collectors.toList());
这样使用map,会编译报错,原因是通过每个老师得到学生列表再转为学生流,这样的结果是一个老师流中,每个老师下又包含了学生流,collect(Collectors.toList())会将老师流转为List,最后返回的是一个List,并不是我们想要的List。
这时,flatMap就发挥作用了,可以将外面的map操作,换为flatMap
List students = teachers.stream().flatMap(teacher -> teacher.getStudents().stream().map(Student::getName)).collect(Collectors.toList());
flatMap会将流中嵌套流这样的情况摊平,也就是把大流内部的小流拆开都放到一个大流中去。
这样我们就得到了所有老师教授的学生列表,如果打印出来,可以发现其中包含重复数据,我们可以在collect之前,做一次去重操作,将重复数据去掉。
List students = teachers.stream().flatMap(teacher -> teacher.getStudents().stream().map(Student::getName)).distinct().collect(Collectors.toList());
得到最大值和最小值
对流中的元素进行判断得到最大值和最小值,流直接提供了max和min方法,调用方法传入比较器就可以了
Optional studentMaxAge = teachers.stream().flatMap(teacher -> teacher.getStudents().stream()).distinct().max(Comparator.comparing(Student::getAge));
Optional studentMinAge = teachers.stream().flatMap(teacher -> teacher.getStudents().stream()).distinct().min(Comparator.comparing(Student::getAge));
沿用上面的例子,如果要获取所有学生中年龄最大的和年龄最小的,可以通过教师得到所有的学生流,然后对学生去重,再调用max和min传入比较器,告知比较器使用年龄做比较,就可以得到最大值和最小值了。相比原来自己写循环遍历,然后得到学生列表,再进行去重,再循环比较,这样的代码可以说清爽太多了。
注意这里的返回值时Optional,这也是Java 8 中提供的类型,它可以包含一个对象,也可以是一个null,因为这里的流可能是空的,如果流为空,就会返回null,为了避免null的检查错误抛出异常,所以max和min返回的都是Optional对象,我们对Optional进行读取,就能得到其中的实际对象了。
判断流中的元素
有些时候,我们需要对流中的元素进行判断,用以获知流中元素是否满足某个条件,是否全部满足某个条件,或者全部不满足某个条件,Java 8的流同样提供了这样的方法,需要对这些方法传入一个谓词,就会对元素进行检查,得到结果。
使用anyMatch,判断是否有元素满足条件
boolean includeYI = students.stream().anyMatch(s->s.contains("乙"));
使用allMatch,判断所有元素都满足条件
boolean allStudenIsAboveTen = teachers.stream().flatMap(teacher -> teacher.getStudents().stream()).distinct().allMatch(student -> student.getAge()>10);
而noneMatch,则刚好与allMatch相反,判断所有元素都不满足条件
得到流中的任意元素和第一个元素
如果,想要得到流中的任意元素可以使用findAny
Optional teacher = teachers.stream().findAny();
想要得到第一个元素可以使用用findFirst
Optional teacher = teachers.stream().findFirst();
这两个方法有什么区别呢?当流顺序执行时,findAny与findFirst返回的结果是一样的,findAny也会得到第一个元素就返回。但是流可以并行运行,当流被拆分到多个线程中,而流本身又是有序的,那么findAny和findFirst得到的结果就不一样了。findFirst还是返回流中第一个元素,而findAny返回的元素就不确定了。
规约流
流在经过多个环节的操作后,往往需要最终得到一个结果,例如求和、连接等。这些操作是把流中的元素进行迭代,将其中的元素反复收纳到一个结果中,我们把这样的操作叫做规约(reduce)。最常见的就是求和。reduce有三个版本的重载:一个参数、两个参数和三个参数
Optional allAge = teachers.stream().flatMap(teacher -> teacher.getStudents().stream()).distinct().map(Student::getAge).reduce((integer, age) -> integer+age);
一个参数的reduce接收一个BinaryOperator的函数式接口,这个接口提供的方法是传入两个相同类型的参数,返回的也是同样的类型的参数:(T,T)->T,这样的方法很适合将两个对象合并成一个对象,至于以什么样的方式合并,就看具体需求了。
这个reduce操作,最基本的操作模型是这样的:先设置一个初始值和一个计算结果,开始时,计算结果就是初始值,然后迭代整个流,将流中的每个元素,与计算结果进行计算得到新的结果,用新的结果更新预设的结果,直到所有的元素迭代完毕,得到的就是最终的结果了。这个描述起来有点绕。举个例子就很容易说明了。
假设一群人要玩一个石头剪刀布的游戏,首先人选一个人,以他作为初始值,我们以游戏里的叫法,可以称其为庄家,其他人逐个进行游戏,庄家与第一个人进行石头剪刀布,若庄家赢了就得到第一个人的筹码,若输了则把筹码给第一个人,赢了的人作为庄家与第二个进行游戏,直到所有人都参与游戏,最后一个人得到了所有的筹码。
这样的过程就很像流的迭代,最开始的庄家,就是要给传给reduce的初始值,后面的石头剪刀布和选出下一个庄家的过程则是传入reduce的行为,也就是Lambda。整个过程就是不断的得到流中的元素,与之前的结果进行计算,得到新的结果,加入下一轮计算中去。
这里面需要注意的是,每次迭代进行计算的结果都与流中的元素一致,如果流中是人,那么计算的结果也是人;如果是数值,计算结果也是数值。另外,reduce的迭代是内部进行的,无需干预,只要给出每次迭代需要执行的行为。
一个参数的reduce,就是将流中的第一个元素作为初始值,进行迭代的操作,这时只要传入一个Lambda表达式就可以了,就像上面的代码中那样。由于一个参数以流中的第一个元素作为初始值,当流中没有元素时,就会出现null的情况,为了避免出现null,一个参数的reduce返回的是Optional。
两个参数的reduce,第一个参数是初始值,这样可以手动指定初始值,其他的与一个参数的reduce一样。但是由于指定了初始值,所以返回值一定不会为null,返回的就是流中元素的类型。
三个参数的reduce,三个参数的reduce比较特殊,第一个参数是初始值,但是其类型可以与流中元素的类型不一致,第二参数是函数式接口——BiFunction,用于指定迭代时执行的行为,第三个参数也是函数式接口——BinaryOperator,用于指定并行运行情况下最终的合并方式。这是reduce最普世的执行方式,可以对流中元素进行操作,返回到另外的一个类型的对象,并且提供了在流进行并行计算时,将多个线程执行的结果进行合并的操作接口。
int allAge = teachers.stream().flatMap(teacher -> teacher.getStudents().stream()).distinct().reduce(0,(integer,student)->integer+student.getAge(),((integer, integer2) -> integer));
这个三个参数的例子中,第一个参数是初始值,第二个是迭代时执行的方法,也就是获得年龄后与前一次的值进行累加,最后一个是并行计算时多个线程运行结果的合并方法,这里不考虑并行计算可以随便返回。