前言:在工作中看到同事写的代码都是随处可见的stream,虽然看不太懂,但是就是感觉很高级的样子,为了融入集体,不得不网上搜了学习。等会用了stream流之后。不禁感慨,真香!!!如果你还未工作,或者是工作后不会使用stream流,那么你必须要学会使用stream流。你可以先不看这篇文章,但我觉得有必要收藏一下,等需要时可以翻出来看看。
参考资料:本文参考了B站上的一门课程,如果有需要的可以移步B站去观看视频。
视频链接
本文会依次学习Stream流的各个方法,为了更好的学习Stream的方法,学习方法之前笔者简单准备了一些开发环境,具体如下:
package com.xlb;
import lombok.EqualsAndHashCode;
import lombok.Getter;
import lombok.Setter;
import lombok.ToString;
import java.util.List;
/**
*班级
*/
@Getter
@Setter
@EqualsAndHashCode
@ToString
public class Class {
private List<Student> students;
}
package com.xlb;
import lombok.EqualsAndHashCode;
import lombok.Getter;
import lombok.Setter;
import lombok.ToString;
@Getter
@Setter
@EqualsAndHashCode
@ToString
public class Course {
public Course() {
}
public Course(String subject, Integer score) {
this.subject = subject;
this.score = score;
}
/**
* 科目
*/
private String subject;
/**
* 分数
*/
private Integer score;
}
package com.xlb;
import lombok.EqualsAndHashCode;
import lombok.Getter;
import lombok.Setter;
import lombok.ToString;
import java.util.List;
@Getter
@Setter
@EqualsAndHashCode
@ToString
public class Student {
public Student() {
}
public Student(String major, String name, Integer age, String gender, String interests, List<Course> courses) {
this.major = major;
this.name = name;
this.age = age;
this.gender = gender;
this.interests = interests;
this.courses = courses;
}
/**
* 专业
*/
private String major;
private String name;
private Integer age;
/**
* 性别
*/
private String gender;
/**
* 兴趣爱好:多个兴趣爱好时逗号分隔(篮球,跑步...)
*/
private String interests;
/**
* 课程
*/
private List<Course> courses;
}
package com.xlb;
import org.junit.Test;
import java.util.ArrayList;
import java.util.List;
public class Main {
@Test
public void testForEach(){
Class studentClass = initTestData();
studentClass.getStudents().stream().forEach(student -> System.out.println(student));
}
@Test
public void testDistinct(){
Class studentClass = initTestData();
}
public static Class initTestData(){
Class studentClass = new Class();
studentClass.setStudents(initStudent());
return studentClass;
}
public static List<Student> initStudent(){
List<Student> students = new ArrayList<>();
Student student1 = new Student("软件工程", "Karry", 18, "男",
"篮球,跑步,游戏", getCourse1());
Student student2 = new Student("软件工程", "Lili", 17, "女",
"cosplay,游戏", getCourse2());
Student student3 = new Student("经济管理", "Query", 27, "男",
"爬山,美食,摄影", getCourse3());
Student student4 = new Student("网络工程", "Windy", 23, "女",
"摄影,绘画", getCourse4());
Student student5 = new Student("软件工程", "Jimi", 19, "女",
"唱歌,游戏", getCourse5());
Student student6 = new Student("土木工程", "Gu", 21, "男",
"爬山,旅游", getCourse6());
students.add(student1);
students.add(student2);
students.add(student2);
students.add(student3);
students.add(student4);
students.add(student4);
students.add(student5);
students.add(student6);
students.add(student6);
return students;
}
public static Student getStudent(){
return new Student("软件工程", "Karry", 18, "男", "篮球,跑步,游戏", getCourse1());
}
public static List<Course> getCourse1(){
List<Course> courses = new ArrayList<>();
Course course1 = new Course("英语", 78);
Course course2 = new Course("高数", 48);
Course course3 = new Course("线代", 63);
Course course4 = new Course("计算机科学", 70);
courses.add(course1);
courses.add(course2);
courses.add(course3);
courses.add(course4);
return courses;
}
public static List<Course> getCourse2(){
List<Course> courses = new ArrayList<>();
Course course1 = new Course("英语", 88);
Course course2 = new Course("高数", 90);
Course course3 = new Course("线代", 89);
Course course4 = new Course("计算机科学", 78);
courses.add(course1);
courses.add(course2);
courses.add(course3);
courses.add(course4);
return courses;
}
public static List<Course> getCourse3(){
List<Course> courses = new ArrayList<>();
Course course1 = new Course("英语", 83);
Course course2 = new Course("高数", 69);
Course course3 = new Course("线代", 66);
Course course4 = new Course("经济管理", 90);
courses.add(course1);
courses.add(course2);
courses.add(course3);
courses.add(course4);
return courses;
}
public static List<Course> getCourse4(){
List<Course> courses = new ArrayList<>();
Course course1 = new Course("英语", 81);
Course course2 = new Course("高数", 76);
Course course3 = new Course("线代", 76);
Course course4 = new Course("网络工程", 80);
courses.add(course1);
courses.add(course2);
courses.add(course3);
courses.add(course4);
return courses;
}
public static List<Course> getCourse5(){
List<Course> courses = new ArrayList<>();
Course course1 = new Course("英语", 70);
Course course2 = new Course("高数", 76);
Course course3 = new Course("线代", 87);
Course course4 = new Course("计算机科学", 90);
courses.add(course1);
courses.add(course2);
courses.add(course3);
courses.add(course4);
return courses;
}
public static List<Course> getCourse6(){
List<Course> courses = new ArrayList<>();
Course course1 = new Course("英语", 69);
Course course2 = new Course("高数", 76);
Course course3 = new Course("线代", 66);
Course course4 = new Course("工程原理", 79);
courses.add(course1);
courses.add(course2);
courses.add(course3);
courses.add(course4);
return courses;
}
}
下面是Stream流一些比较常用的方法,笔者主要就从这些方法开始讲起,因为方法比较多,可能会分两三篇文章进行介绍。
笔者将Stream的方法分为了三个部分,即基础方法,必会方法和常用方法。其中基础方法使用起来很简单,必会方法是笔者在工作中使用频率非常高的方法且使用起来相对有点难度。
先看下我们范例的结构,笔者定义了一个班级类,班级类内部有一个学生集合,学生类内部有各种属性,并且其中有一个课程结合的属性,然后有一个兴趣爱好的属性,多个兴趣爱好的话使用","分隔,课程类内部包含两个属性。在具体测试前,笔者先调用初始化方法初始化所需的内部数据,具体数据可以看上面代码。
先看下forEach接口声明
看下方法形参Consumer泛型
可以看到Consumer
接口内部提供了一个接口方法和一个默认方法,像这种接口内部有且只有一个抽象方法
的接口称之为函数式接口
。一般函数式接口在类上方会有@FunctionalInterface标识,(如果没有该标识接口也可能是一个函数式接口,不过用这个注解后如果类不符合函数式接口的定义,则该注解会报错)。
注意:函数式接口的定义是接口内部有且仅有一个抽象方法,不管接口内默认方法的个数,
那函数式接口有什么用?函数式接口的出现是因为只要是接口是一个函数式接口,那么他就可以使用lambda表达式,从而使代码看起来更加优雅。如果使用IDEA的话,匿名内部类和lambda之间可以使用快捷键Alt+Enter进行转化
forEach方法和集合本身的forEach方法一样都是对集合元素进行遍历。
使用匿名内部类的方式使用forEach
@Test
public void testForEachV1(){
Class studentClass = initTestData();
studentClass.getStudents().stream().forEach(new Consumer<Student>() {
@Override
public void accept(Student student) {
System.out.println(student);
}
});
}
使用lambda的方式使用forEach
@Test
public void testForEachV2(){
Class studentClass = initTestData();
studentClass.getStudents().stream().forEach(student -> System.out.println(student));
}
结果输出
stream流式处理就是将集合元素变成一个一个的元素,像水流一样依次经过stream方法进行处理。如上图所示,forEach就是遍历所有的元素信息。
使用
@Test
public void testLimit(){
Class studentClass = initTestData();
studentClass.getStudents().stream().limit(3).forEach(student -> System.out.println(student));
}
可以看到,只输出了三个元素,Stream的limit方法就是指定输出集合中的前n个元素;如果n大于集合中的元素个数,则输出集合中的全部元素。
@Test
public void testSkip(){
Class studentClass = initTestData();
studentClass.getStudents().stream().skip(1).forEach(student -> System.out.println(student));
}
skip方法类似于offset,即偏移量,skip(1)就等同于略过第一个元素,从第二个元素开始处理。
@Test
public void testCount(){
Class studentClass = initTestData();
System.out.println(studentClass.getStudents().stream().count());
}
count就是计算当前流处理的元素的个数。
distinct方法就是去重。笔者先使用distinct方法,然后再使用count方法,看一下元素个数。
@Test
public void testDistinct(){
Class studentClass = initTestData();
long count = studentClass.getStudents().stream()
.distinct()
.count();
System.out.println(count);
}
调试+输出
笔者利用IDEA的调试工具,一步步的看stream流的处理过程如下,刚开始集合中有8个元素,经过distinct处理之后还剩6个元素,然后通过count计算元素个数输出。
filter方法即是一个过滤条件,符合条件的元素会保留,不符合条件的元素会剔除。
/**
* 过滤出年龄大于21岁的元素并输出
*/
@Test
public void testFilterV1(){
Class studentClass = initTestData();
studentClass.getStudents().stream()
.filter(student -> student.getAge().compareTo(21) > 0)
.forEach(student -> System.out.println(student));
}
/**
* 匿名内部类实现:过滤出年龄大于21岁的元素
*/
@Test
public void testFilterV2(){
Class studentClass = initTestData();
studentClass.getStudents().stream()
.filter(new Predicate<Student>() {
@Override
public boolean test(Student student) {
return student.getAge().compareTo(21) > 0;
}
})
.forEach(student -> System.out.println(student));
}
输出
/**
* 过滤出年龄大于21岁的元素,并输出具体的年龄
*/
@Test
public void testMap(){
Class studentClass = initTestData();
studentClass.getStudents().stream()
.filter(student -> student.getAge().compareTo(21) > 0)
.map(student -> student.getAge())
.forEach(age -> System.out.println(age));
}
输出
/**
* 过滤出年龄大于21岁的元素,并输出具体的年龄
*/
@Test
public void testMapToInt(){
Class studentClass = initTestData();
studentClass.getStudents().stream()
.filter(student -> student.getAge().compareTo(21) > 0)
.mapToInt(student -> student.getAge())
.forEach(age -> System.out.println(age));
}
/**
* 过滤出年龄大于21岁的元素,并输出具体的年龄
*/
@Test
public void testMapToIntV2(){
Class studentClass = initTestData();
studentClass.getStudents().stream()
.filter(student -> student.getAge().compareTo(21) > 0)
.mapToInt(new ToIntFunction<Student>() {
@Override
public int applyAsInt(Student student) {
return student.getAge();
}
})
.forEach(age -> System.out.println(age));
}
可以看出目前使用map和mapToInt两个方法并没有什么不同,都可以正常输出整数。不同点在于mapToInt返回的是IntStream流,map返回的是Stream流,mapToInt返回的值直接就是int
类型。之所以使用mapToInt是因为如果是Interger类型,那么是存在自动装箱和自动拆箱的过程,如果在流式运算中有数值比较,使用mapToInt可以避免自动装箱和自动拆箱提高效率。
/**
* 有任何一门分数大于90分的科目就是优秀学生,输出所有的优秀学生
*/
@Test
public void testAnyMatch(){
Class studentClass = initTestData();
studentClass.getStudents().stream()
.filter(
student -> student
.getCourses()
.stream()
.anyMatch(course -> course.getScore().compareTo(90) > 0)
)
.forEach(student -> System.out.println(student));
}
输出
/**
* 有任何一门分数大于95分且没有未及格的科目就是模范学生,输出所有的模范学生
*/
@Test
public void testAllMatch(){
Class studentClass = initTestData();
studentClass.getStudents().stream()
.filter(
student -> student
.getCourses()
.stream()
.anyMatch(course -> course.getScore().compareTo(90) > 0)
)
.filter(
student -> student
.getCourses()
.stream()
.allMatch(course -> course.getScore().compareTo(60) > 0)
)
.forEach(student -> System.out.println(student));
}
输出
/**
*找出兴趣不包含游戏的所有学生并打印
*/
@Test
public void testNoneMatch(){
Class studentClass = initTestData();
studentClass.getStudents().stream()
.filter(student -> Arrays.stream(student.getInterests().split(",")).noneMatch(interest -> "游戏".equals(interest)))
.forEach(student -> System.out.println(student));
}
输出
对于上面这个使用到了Arrays.stream(),这个方法,可能有些朋友对这个比较陌生。之所以使用这个是因为stream流是对集合进行的操作。我们通过split将兴趣爱好的字符串拆分成了兴趣爱好的数组,但是数据并没有stream方法,不过Arrays提供了一个静态方法stream,使得我们同样可以将stream流运用在数组上。
可以看到reduce有三个重载方法,我们依次使用这三个方法。
第一个reduce重载方法
/**
*规约方法:对所有的年龄求和
*/
@Test
public void testReduce(){
Class studentClass = initTestData();
int sum = studentClass.getStudents().stream()
.distinct()
.map(student -> student.getAge())
.reduce(0, (result, age) -> result + age);
System.out.println(sum);
}
/**
*规约方法:对所有的年龄求和(方法引用)
*/
@Test
public void testReduceV2(){
Class studentClass = initTestData();
int sum = studentClass.getStudents().stream()
.distinct()
.map(Student::getAge)
.reduce(0, Integer::sum);
System.out.println(sum);
}
/**
* 所有年龄最小值
*/
@Test
public void testReduceV3(){
Class studentClass = initTestData();
int sum = studentClass.getStudents().stream()
.distinct()
.map(Student::getAge)
.reduce(Integer.MAX_VALUE, Integer::min);
System.out.println(sum);
}
第二个reduce重载方法
/**
* 所有年龄最大值
*/
@Test
public void testReduceV4(){
Class studentClass = initTestData();
Optional<Integer> res = studentClass.getStudents().stream()
.distinct()
.map(Student::getAge)
.reduce((integer, integer2) -> integer.compareTo(integer2) > 0 ? integer : integer2);
res.ifPresent(value -> System.out.println(value));
}
@Test
public void testReduceV4(){
Class studentClass = initTestData();
Optional<Integer> res = studentClass.getStudents().stream()
.distinct()
.map(Student::getAge)
.reduce(new BinaryOperator<Integer>() {
@Override
public Integer apply(Integer integer, Integer integer2) {
return integer.compareTo(integer2) > 0 ? integer : integer2;
}
});
res.ifPresent(value -> System.out.println(value));
}
这个方法中我们看到返回了Optional对象,Optional对象属于一种安全的对象,其出现的原因就是为了防止写代码时由于疏忽出现的空指针异常。
@Test
public void testReduceV7(){
Class studentClass = initTestData();
String reduce = studentClass.getStudents().stream()
.distinct()
.map(Student::getName)
.reduce("", new BinaryOperator<String>() {
@Override
public String apply(String s, String s2) {
String format = String.format("s:%s, s2:%s", s, s2);
System.out.println(String.format("s:%s, s2:%s", s, s2));
return s+s2;
}
}, new BinaryOperator<String>() {
@Override
public String apply(String integer, String integer2) {
System.out.println(String.format("interger:%s, interger2:%s", integer, integer2));
return integer + integer2;
}
});
System.out.println(reduce);;
}
输出
从上面的输出我们好像看不出来第三个参数有什么作用,而且第三个参数好像没有执行(因为内部的数据并没有打印出来),我们把流换成并行流试试。
@Test
public void testReduceV8(){
Class studentClass = initTestData();
String reduce = studentClass.getStudents().parallelStream()
.distinct()
.map(Student::getName)
.reduce("", new BinaryOperator<String>() {
@Override
public String apply(String s, String s2) {
String format = String.format("s:%s, s2:%s", s, s2);
System.out.println(String.format("s:%s, s2:%s", s, s2));
return s+s2;
}
}, new BinaryOperator<String>() {
@Override
public String apply(String integer, String integer2) {
System.out.println(String.format("interger:%s, interger2:%s", integer, integer2));
return integer + integer2;
}
});
System.out.println(reduce);;
}
可以看到第三个数据里的参数被打印出来了,并且最后的输出数据和上面不是并行流的输出数据相同。所以reduce的第三个参数是在并行流中才起作用,而且是对之前数据的组装。
把线程名字也打印出来,如下
总结:因为stream流的方法比较多,所以剩下的常用方法笔者在下一篇文章介绍。通过上面的使用可能大家都会发现Stream流的方法有的返回的是Stream本身,有的返回的不是Stream(如果返回boolean,int等),那么返回Stream本身的我们可以接着调用Stream流的其他操作,返回的不是Stream流本身时我们就调用不了Stream的操作了。所以Stream流的方法有两个叫法:一是终端操作,而是非终端操作。终端操作即是返回的非Stream流,之后我们不得不停止对Stream流的操作。非终端操作方法执行后返回的还是Stream流,我们可以接着执行Stream流方法,也即是可以继续链式调用的执行。