编码五分钟,摸鱼两小时,Stream 你学废了吗?

102 阅读6分钟

一起养成写作习惯!这是我参与「掘金日新计划 · 4 月更文挑战」的第4天,点击查看活动详情

前言

在 JDK 1.8 之前,如果我们要处理集合里面的数据,常常会使用 for 循环及逆行操作 众所周知,for 循环不多的话看起来还好,如果来几个层级的嵌套,加上几个 if,阅读性会变得非常擦,特别是处理逻辑稍微复杂一点的时候,不仅难看,还容易因为粗心而出现 Bug

在 JDK 1.8,引入了 Stream 流式编程,可以明显感觉编码效率大大的提升,写出来的代码也清爽干净很多

但需要注意的是,如果在流式变成内写入复杂的逻辑,同样也会让代码变得难懂,流式编程只是一种手段,如果写代码的人本身就很注重代码的可读性,哪怕他不用 Stream,任然可以写出非常干净清爽且易懂的代码

什么是 Stream

Java8 中的 Stream 是对容器对象功能的增强,它专注于对容器对象进行各种非常便利、高效的聚合操作,或者大批量数据操作

我们用流式这个词来理解,我们想象集合内的元素都是水,我们使用 Stream 让这些水在一个管道中流动起来,我们可以在管道中设置不同功能的节点,比如排序、筛选、聚合等等操作,最后在管道的终点得到最终的结果

整个 Stream 流分为三个阶段:

  1. 创建一个流
  2. 中间操作
  3. 终止操作

如何创建一个流

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 函数又长又臭,在代码评审会议中会被拉出来鞭尸噢,别问我为什么知道,我就是知道!