简历
Java 8 的新特性:Stream
(中文称之为:流)也是很常见且常用的,主要优点是提升开发效率,使代码更干净、简洁。
Stream
的主要作用是对集合(Collection
)中的数据进行各种操作,增强了集合对象的功能。
流迭代
在 Java 中,Stream
是一个接口(interface
),当然也有多个实现类。但 Java 是面向接口编程的,我们暂时不用关心具体有哪些实现类,先学会使用 Stream
API
。
在接口中提供了操作数据的方法,通常我们叫作 API
创建流
创建的流的方式常见的有多种:
一、直接创建
import java.util.stream.Stream;
Stream<String> stream = Stream.of("苹果", "哈密瓜", "香蕉", "西瓜", "火龙果");
二、由数组转化
String[] fruitArray = new String[] {"苹果", "哈密瓜", "香蕉", "西瓜", "火龙果"};
Stream<String> stream = Stream.of(fruitArray);
三、由集合转化
List<String> fruits = new ArrayList<>();
fruits.add("苹果");
fruits.add("哈密瓜");
fruits.add("香蕉");
fruits.add("西瓜");
fruits.add("火龙果");
Stream<String> stream = fruits.stream();
无论是哪种方式创建,一定要清楚的知道:由于源数据(集合或数组)的数据是有序的,所以流中的元素也是有序排队的。
迭代流
集合类提供了 forEach()
方法可以遍历集合中的元素。很巧的是,Stream
提供的迭代方法也叫做 forEach()
。
Stream<String> stream = Stream.of("苹果", "哈密瓜", "香蕉", "西瓜", "火龙果");
stream.forEach(System.out::println);
注意:为了能让系统自动识别 Lambda 表达式的参数类型,也必须使用泛型语法指定 Stream
中对象的类型。
Stream API
有很多,功能十分强大,迭代流只是最基础的应用
注意:
集合类的 forEach()
方法与流的 forEach()
方法无关,仅仅是方法名相同而已。不要混淆哦。
流数据过滤
例:
湖北省襄阳市红旗区向阳小学正在评选优秀学生,要求是期末考试平均分不低于 80 分且无违规记录,才有资格参与评选。
小学生的模型是:
public class Pupil {
private String name;
// 平均分
private int averageScore;
// 违规次数
private int violationCount;
}
name | 平均分 | 违规次数 |
司音 | 75 | 1 |
白浅 | 80 | 0 |
荀飞盏 | 95 | 8 |
墨渊 | 79 | 0 |
夜华 | 90 | 0 |
霓漫天 | 81 | 0 |
问题
如何统计哪些小学生有参与评选的资格呢?
传统实现
一般的代码是这样的:
List<Pupil> pupils = new ArrayList<>();
// 这里假设小学生数据对象已经存入了
// 有资格的小学生集合
List<Pupil> qualified = new ArrayList<>();
for (Pupil pupil : pupils) {
// 统计是否满足条件
if (pupil.getAverageScore() >= 80 && pupil.getViolationCount() < 1) {
qualified.add(pupil);
}
}
// 打印符合条件的小学生姓名
for (Pupil pupil : qualified) {
System.out.println(pupil.getName());
}
新特性实现
用 Java8 新特性完成统计:
List<Pupil> pupils = new ArrayList<>();
// 这里假设小学生数据对象已经存入了
pupils.stream()
.filter(pupil -> pupil.getAverageScore() >= 80 && pupil.getViolationCount() < 1)
.forEach(pupil -> {System.out.println(pupil.getName());});
filter() 方法
从方法名我们可以理解其功能:对流中的数据对象进行过滤。
方法参数是一个 Lambda 表达式,箭头后面是条件语句,判断数据需要 符合 的条件。
也就是说,使用 Lambda 表达式告诉过滤器,需要那些 符合 条件的数据(把不符合条件的数据过滤掉)。
注意:这里的 Lambda 表达式略有不同。
箭头后的过滤条件语句(非可执行的语句)。传统代码中,条件语句写在 () 中的,所以新特性条件语句可以用 () 而不能用 {} 哦。
等同于 pupil -> ((pupil.getAverageScore() >= 80) && (pupil.getViolationCount() < 1))
流的设计思想(一)
数据流的操作过程,可以看做一个管道,管道由多个节点组成,每个节点完成一个操作。
数据流输入这个管道,按照 顺序 经过各个节点。
流数据映射
小习题:对于一组数字:
List<Integer> numbers = Arrays.asList(3, 2, 2, 7, 63, 2, 3, 5);
计算每个数字的平方数并输出。
解答
使用 Stream API
就很简便了:
numbers.stream()
.map(num -> {
return num * num;
})
.forEach(System.out::println);
这里用到的,是 map()
方法。
map()
方法通常称作映射,其作用就是用新的元素替换掉流中原来相同位置的元素。相当于每个对象都经历一次转换。
映射到新数据
map()
方法的参数是一个 Lambda 表达式,在语句块中对流中的每个数据对象进行计算、处理,最后用 return
语句返回的对象,就是转换后的对象。
优点
映射后的对象类型,可以与流中原始的对象类型不一致。
在流中,可以用字符串替换原来的整数。这就极大的提供了灵活性、扩展性,让流的后继操作可以更方便。
特例写法
少数情况下,如果替换语句简单、系统能自动识别需要返回的值,代码可以简写为:
.map(num -> num * num)
流数据排序
对于数据的处理,排序是很常见的操作。在上一章第二节课中,我们学会了使用 Lambda 表达式优化语句:
List<Student> students = new ArrayList<Student>();
students.add(new Student(111, "bbbb", "london"));
students.add(new Student(131, "aaaa", "nyc"));
students.add(new Student(121, "cccc", "jaipur"));
// 实现升序排序
Collections.sort(students, (student1, student2) -> {
// 第一个参数的学号 > 第二个参数的学号
return student1.getRollNo() - student2.getRollNo();
});
students.forEach(s -> System.out.println(s));
使用 Stream API
就更简便了:
students.stream()
// 实现升序排序
.sorted((student1, student2) -> {
return student1.getRollNo() - student2.getRollNo();
})
.forEach(System.out::println);
sorted()
顾名思义,就是完成排序的方法。把排序规则写成一个 Lambda 表达式传给此方法即可。
参数顺序
无论是 Lambda 表达式 写法,还是 Stream API 写法,参数 (student1, student2)
中的 student1
和 student2
分别指代第几个元素呢?
答案是:student1 指代后一个元素,student2 指代前一个元素。不要被变量名迷惑了哦。
第一次遍历时,student1 指代第二个名称为 aaaa 的学生,student2 指代第一个名称为 bbbb 的学生。
特例写法
少数情况下,如果排序计算语句简单、系统能自动识别需要返回的值,代码可以简写为:
.sorted((student1, student2) -> student1.getRollNo() - student2.getRollNo())
小知识点
不知道大家总结过没有,为什么无论是集合排序 Collections.sort()
还是流排序 sorted()
,都需要返回一个数值呢?返回数值跟排序升降有啥关系呢?
本节课案例中,前两个学生的学号相减 131-111 = 20
,为什么返回 20
就表示升序呢?
因为,返回 非正数 表示两个相比较的元素需要 交换位置 ,返回正数则不需要。
131-111 = 20
表示前两个学生不需要改变在集合中的位置顺序。
那么后两个学生进行比较:
121-131 = -10
返回了负数,于是名称为 aaaa
的学生需要与名称为 cccc
的学生交换位置、改变顺序,完成了由小到大的升序排序。
那么如何进行降序排序呢?
同理 使用 student2.getRollNo() - student1.getRollNo()
。
集合中前两个学生 111-131=-20
,此时返回的结果为非正数,所以两者交换位置
后两个学生进行比较 111-121=-10
返回了负数,所以两者交换顺序,最终变成了 131->121->111
的集合顺序,完成降序排序。
流数据摘取
小习题:对于一组数字:
List<Integer> numbers = Arrays.asList(3, 2, 2, 7, 63, 2, 3, 5);
找出最大的前 3 个数字。
答案
我们马上可以想到使用排序就能知道哪个数字最大。
再此基础上,Stream API
提供了摘取接口,很容易完成:
numbers.stream()
.sorted((n1, n2) -> n2 - n1)
.limit(3)
.forEach(System.out::println);
limit()
方法的作用是返回流的 前 n
个元素,当然 n
不能为负数。
不是摘取任意位置哦,只能是流开头的
流的设计思想(二)
对比普通 Java 代码,Stream
的显著特点是:编程的重点,不再是对象的运用,而是数据的计算。
如果使用普通的 Java 代码,重点是使用对象完成各种各样的逻辑,加上语法代码也比较多,导致整个代码比较繁复。
使用了 Stream API
,系统会自动完成很多操作,加上大幅度简化了语法,开发者的注意力重点就变为捋清楚数据计算的步骤,不用太关心变量类型、变量赋值、对象转换等。编程的重点更加清晰。
Stream 的这种变化,特征是:函数式风格。即弱化了面向对象的严格、完整的语法,重心变为通过函数完成数据计算。