一起养成写作习惯!这是我参与「掘金日新计划 · 4 月更文挑战」的第4天,点击查看活动详情。
前言
在 JDK 1.8 之前,如果我们要处理集合里面的数据,常常会使用 for 循环及逆行操作 众所周知,for 循环不多的话看起来还好,如果来几个层级的嵌套,加上几个 if,阅读性会变得非常擦,特别是处理逻辑稍微复杂一点的时候,不仅难看,还容易因为粗心而出现 Bug
在 JDK 1.8,引入了 Stream 流式编程,可以明显感觉编码效率大大的提升,写出来的代码也清爽干净很多
但需要注意的是,如果在流式变成内写入复杂的逻辑,同样也会让代码变得难懂,流式编程只是一种手段,如果写代码的人本身就很注重代码的可读性,哪怕他不用 Stream,任然可以写出非常干净清爽且易懂的代码
什么是 Stream
Java8 中的 Stream 是对容器对象功能的增强,它专注于对容器对象进行各种非常便利、高效的聚合操作,或者大批量数据操作
我们用流式这个词来理解,我们想象集合内的元素都是水,我们使用 Stream 让这些水在一个管道中流动起来,我们可以在管道中设置不同功能的节点,比如排序、筛选、聚合等等操作,最后在管道的终点得到最终的结果
整个 Stream 流分为三个阶段:
- 创建一个流
- 中间操作
- 终止操作
如何创建一个流
1)通过数组创建一个流
Integer[] ints = {1, 2, 3, 4, 5};
Stream<Integer> stream = Arrays.stream(ints);
2)使用集合创建一个流
List<Integer> aList = Arrays.asList(1, 2, 3, 4, 5);
Stream<Integer> stream = aList.stream();
3)创建一个并行流
List<Integer> aList = Arrays.asList(1, 2, 3, 4, 5);
Stream<Integer> stream = aList.parallelStream();
//或者
Stream<Integer> stream = aList.stream().parallel();
4)使用 Stream 创建流
可以使用 Stream 类里面的静态方法创建一个流,像是
Stream.iterate()、Stream.of()、Stream.generate()方法都可以用来创建流,下面的代码块是这三个方法的一个示例
Stream<Integer> stream = Stream.iterate(1, (x) -> x + 1).limit(5);
Stream<Integer> stream = Stream.of(1, 2, 3, 4, 5);
Stream<Integer> stream = Stream.generate(() -> 1).limit(3);
Stream 的中间操作
每个管道节点对流进行处理后,会返回一个新的流,交给下一个节点使用,一个流可以有多个中间层操作
需要注意的是,这类操作都是惰性的,并没有对流进行真正的遍历,而是在终止操作时一次性全部处理,也被称为“惰性求值”
Stream 的中间操作在整体上可以分为:筛选与切片、映射、排序
为了方便我们学习,先创建一个用于测试的学生集合,然后向其中添加一些测试数据
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.util.Arrays;
import java.util.List;
public class StreamOperate {
@Data
@AllArgsConstructor
@NoArgsConstructor
static class Student {
String name;
Integer age;
String address;
}
public static void main(String[] args) {
List<Student> list = Arrays.asList(
new Student("张三", 18, "长沙"),
new Student("李四", 38, "北京"),
new Student("王五", 60, "上海"),
new Student("陈六", 38, "深圳"),
new Student("陈六", 38, "深圳")
);
}
}
筛选与切片
filter()- 接受 Lambda 表达式,从流中排除符合条件的元素distinct()- 筛选,通过流所生成元素的hashCode()和equals()去除重复元素limit()- 截断流,使其元素不超过给定的数量skip()- 跳过元素,返回一个扔掉了前面给定数量元素的流,若流中元素数量不足,则返回一个空的流
示例:
1)filter()
过滤出年龄大于18岁的数据
list.stream().filter(e -> e.age >= 18).forEach(System.out::println);
输出:
StreamOperate.Student(name=张三, age=18, address=长沙)
StreamOperate.Student(name=李四, age=38, address=北京)
StreamOperate.Student(name=王五, age=60, address=上海)
StreamOperate.Student(name=陈六, age=38, address=深圳)
StreamOperate.Student(name=陈六, age=38, address=深圳)
2) distinct()
过滤出年龄大于18岁的数据,再去掉重复的数据
list.stream().filter(e -> e.age >= 18).distinct().forEach(System.out::println);
输出:
StreamOperate.Student(name=张三, age=18, address=长沙)
StreamOperate.Student(name=李四, age=38, address=北京)
StreamOperate.Student(name=王五, age=60, address=上海)
StreamOperate.Student(name=陈六, age=38, address=深圳)
3)limit()
截取前面 n 条数据
list.stream().limit(2).forEach(System.out::println);
输出:
StreamOperate.Student(name=张三, age=18, address=长沙)
StreamOperate.Student(name=李四, age=38, address=北京)
4)skip()
跳过前面 n 条数据,跟 limit 的作用正好相反
list.stream().skip(2).forEach(System.out::println);
输出:
StreamOperate.Student(name=王五, age=60, address=上海)
StreamOperate.Student(name=陈六, age=38, address=深圳)
StreamOperate.Student(name=陈六, age=38, address=深圳)
可以看到,在新的流里面,张三和李四被跳过了
映射
map()- 接收一个函数作为参数,该函数会被应用到每个元素上,并将其映射成一个新的元素flatMap()- 接收一个函数作为参数,将流中的每个值都换成另一个流,然后把所有流连接成一个流
示例:
1)map()
接收一个函数作为参数,该函数会被应用到每个元 素上,并将其映射成一个新的元素
下面的例子可以获取所有人的名字并映射到一个新的元素上,新元素组成一个新的流
list.stream().map(e -> e.getName()).forEach(System.out::println);
输出:
张三
李四
王五
陈六
陈六
2)flatMap()
接收一个函数作为参数,将流中的每个值都换成另 一个流,然后把所有流连接成一个流
下面的例子可以拿到 str 中的所有字母
String str = "i love u";
Stream.of(str.split(" "))
.flatMap(e -> e.chars().boxed())
.forEach(i -> System.out.println((char)i.intValue()));
排序
sorted()- 产生一个新流,其中按自然顺序排序,可以传入比较器,让新流按照比较器的顺序排序
示例:
使用年龄排序,然后使用姓名排序
list.stream().sorted(Comparator.comparing(Student::getAge).thenComparing(Student::getName)).forEach(System.out::println);
输出:
StreamOperate.Student(name=张三, age=18, address=长沙)
StreamOperate.Student(name=李四, age=38, address=北京)
StreamOperate.Student(name=陈六, age=38, address=深圳)
StreamOperate.Student(name=陈六, age=38, address=深圳)
StreamOperate.Student(name=王五, age=60, address=上海)
Stream 的终止操作
一个流只能有一个终止操作,该操作会返回最终的执行结果,这个阶段才会正真的去遍历流
collect()- 将元素收集到一个新的集合中reduce()- 累计求和max() / min()- 获取最大或者最小值count()- 计数toArray()- 将流中的元素转换为一个数组forEach() / forEachOrdered()- 遍历流中的元素,也可以指定遍历的顺序anyMatch / allMatch() / noneMatch- 返回一个布尔值,判断里面有没有符合条件的元素findFirst() / findAny()- 取出流中的元素,如果流中没有元素,findAny 方法会返回一个空的 Optional
示例:
1) 将处理后的流生成一个新的集合
List<String> collect = list.stream().map(Student::getName)
.collect(Collectors.toList());
System.out.println(collect);
输出:
[张三, 李四, 王五, 陈六, 陈六]
2)基于 reduce() 得到所有人的年龄总和
Integer reduce = list.stream().map(Student::getAge).reduce(0, (a1, a2) -> a1 + a2);
System.out.println(reduce);
输出:
192
3)基于max() / min() 获取年龄最大或最小的信息
Optional<Student> max = list.stream().max(Comparator.comparingInt(Student::getAge));
System.out.println(max.get());
输出:
StreamOperate.Student(name=王五, age=60, address=上海)
4)判断集合里面有没有等于十八岁的信息
boolean b1 = list.stream().anyMatch(e -> e.getAge() == 18);
System.out.println(b1);
输出:
true
总结一下
Stream 结合 Lambda 表达式,能大大提高我们编码的效率,但就跟喝酒一样,可千万不要贪杯啊,如果你的 Lambda 函数又长又臭,在代码评审会议中会被拉出来鞭尸噢,别问我为什么知道,我就是知道!