Java中Stream相关常用总结记录

Java中Stream相关常用总结记录

阅读本篇文章之前最好有lambda表达式和方法引用基础

什么是流?

首先贴一下比较官方的流的说法:

  1. 不存储数据:流是基于数据源的对象,它本身不存储数据元素,而是通过管道将数据源的元素传递给操作。
  2. 函数式编程:流的操作不会修改数据源,例如filter不会将数据源中的数据删除
  3. 延迟操作: 惰性求值,流在中间处理过程中,只是对操作进行了记录,并不会立即执行,需要等到执行终止操作的时候才会进行实际的计算。
  4. 可以解绑:对于无限数量的流,有些操作是可以在有限的时间完成的,比如limit(n) 或 findFirst(),这些操作可是实现"短路"(Short-circuiting),访问到有限的元素后就可以返回。
  5. 纯消费:流的元素只能访问一次,类似iterator,操作没有回头路,如果你想从头访问一遍流的元素,那必须重新生成一个流。
    流的操作是以管道方式串起来的,流管道包含一个数据源,接着包含0-n个中间操作,最后包含一个终点操作结束。

可能说的不是很专业,但我会尽量描述清楚,而且比较通俗易懂,应该不会有太大的偏差。

所谓流,我所理解的就是一种很舒服的针对集合容器中数据读取操作的方式,且提供了函数式编程的方式,话说,在java8出现之前,没有这个概念,但是还是不影响我们操作数据,只是可能会不够“优雅”罢了,这里说的优雅,是相对的,因为有时候这种“优雅”反而会适得其反。

如何创建一个流?

Java中创建一个流很简单,只需调用集合对象parallelStream()或者stream(),前者是并行流,后者是普通流。

所谓并行流,就是开启了多线程执行,一般不推荐使用,因为不好控制并发安全

List<Integer> list = new ArrayList<>();
//普通流
list.stream();
//并行流
list.parallelStream();

本文接下来默认使用普通流创建

如何关闭一个流?

既然我们知道了如何创建一个流,那么如何将流关闭呢,也就是结束掉流,毕竟我们的目的是想要通过流来帮我们操作集合中的元素,但我们操作完成后想获取结果,这时候流也就终止了。

我们可以看到,但我们创建一个流之后,返回的其实也是一个流对象,它是泛型的。

List<Integer> list = new ArrayList<>();
Stream<Integer> stream = list.stream();

我们可以使用以下方式终止流,来获取到我们想要的数据。

注:在后面我会详细说明各种方法的使用常见

  1. 查找与匹配

    方法名 描述
    allMatch(Predicate p) 检查是否匹配所有元素(当流中没有任何元素,会返回true)
    anyMatch(Predicate p) 检查是否至少匹配一个元素(当流中没有任何元素,会返回和false)
    noneMatch(Predicate p) 检查是否没有匹配所有元素(当流中没有任何元素,会返回和false)
    findFirst() 返回第一个元素
    findAny() 返回当前流中的任意元素
    max(Comparator c) 返回流中最大值
    min(Comparator c) 返回流中最小值
    forEach(Consumer c) 内部迭代(使用 Collection 接口需要用户去做迭代,称为外部迭代。相反,Stream API 使用内部迭代——它帮你把迭代做了)
  2. 归约

    方法名 描述
    reduce(T iden, BinaryOperator b) 可以将流中元素反复结合起来,得到一个值。返回 T
    reduce(BinaryOperator b) 可以将流中元素反复结合起来,得到一个值。返回 Optional< T >
  3. 收集

    对应是collect(Collectors.xxx())方法

    方法名 描述
    toList 把流中元素收集到List
    toSet 把流中元素收集到Set
    toCollection 把流中元素收集到创建的集合
    counting 计算流中元素的个数
    summingInt 对流中元素的整数属性求和
    averagingInt 计算流中元素Integer属性的平均值
    summarizingInt 收集流中Integer属性的统计值。如:平均值
    joining 连接流中每个字符串
    maxBy 根据比较器选择最大值
    minBy 根据比较器选择最小值
    reducing 从一个作为累加器的初始值开始,利用BinaryOperator与流中元素逐个结合,从而归约成单个值
    collectingAndThen 包裹另一个收集器,对其结果转换函数
    groupingBy 根据某属性值对流分组,属性为K,结果为V
    partitioningBy 根据true或false进行分区

常用流的使用

一个集合转另一个集合(泛型不同)

这是我最经常用的方法

比如我这里有个学生类集合,我只需要他们的学号集合,我们就可以利用map方法

map方法可以理解为映射,可以根据需求将原有的流类型统一映射为另外的类型

注意:在流中的操作都是依次的,它会针对集合中的所有元素依次操作

首先创建一个学生类

@Data
@AllArgsConstructor
@NoArgsConstructor
public static class Student {
    private Integer num;
    private String name;
    private Integer age;
    private String className;
}

将学生类集合转换学生学号集合

List<Student> studentList = new ArrayList<>();
studentList.add(new Student(1, "周杰伦", 42, "三年二班"));
studentList.add(new Student(2, "林俊杰", 42, "三年二班"));
//原始学生集合
System.out.println(studentList);
//利用学生集合创建流,然后把学生流重新映射为学生类里面的"Num"也就是Integer流
Stream<Integer> integerStream = studentList.stream().map(student -> {
    return student.getNum();
});
//当然上面的写法可以简化为:
Stream<Integer> integerStreamSimplify = studentList.stream().map(Student::getNum);

//别忘了操作完流后,我们需要关闭流再将它转换成需要的数据
//将Integer流转换为Integer集合
List<Integer> studentNumList = studentList.stream().map(Student::getNum).collect(Collectors.toList());

别忘了collect(Collectors.toList())方法的意思是结束流,同时把流转换为对应的List集合。

将泛型为List的集合平铺(转成一个)

想象一个给你一个List<List<Integer>>类型的集合,你需要把它转换为List<Integer>类型的集合

你可能首先想到这样做:

List<List<Integer>> doubleIntegerList = new ArrayList<>();
List<Integer> integerList = new ArrayList<>();
doubleIntegerList.forEach(integerList::addAll);

这样做确实可以,但个人总觉得不太优雅,因为要额外创建一个集合来一个个的addAll

如果我们使用流的flatMap来做就不需要额外事先创建一个集合了

flatMap其实是对应map的,上面说到map是从一个对象流映射到另一个对象流,而如果你的对象流本身也可以生成流,就可以通过它将你生成的对象流统一收集

List<List<Integer>> doubleIntegerList = new ArrayList<>();
List<Integer> result = doubleIntegerList.stream().flatMap(integerList -> {
  	//返回一个流
    return integerList.stream();
}).collect(Collectors.toList());

值得一提的是,这种平铺集合的方式使用流的效率在极端情况下其实并不高,但是它让我们的代码更简洁(可能也少了一些可读性)

下面是上面两种方法性能的对比:

image-20220623163509751

可以看到,在有1000*10000规模的条件下,用流的方式效率低了差不多有5倍

将集合转为Map

看似不起眼,实际上用熟练了非常好用的方法。

有时候需要遍历集合,然后再遍历过程中查询数据,这样会频繁的查询数据库,非常影响性能,所以推荐提前查询所有可能用得到的数据。

我最常用的使用场景就是作为批量查询数据缓存,通常是利用id作为key,方便后续直接取用.

List<Student> studentList = new ArrayList<>();
Map<Integer, Student> studentMap = studentList.stream().collect(Collectors.toMap(student -> {
    return student.getNum();
}, student -> student));

toMap()方法需要两个参数,它们都可以是lambda表达式,第一个为key,不能重复,第二个为value

以上代码实际上可以简写为下面的形式

//模拟一次性批量从数据库查询所有数据
List<Student> studentList = new ArrayList<>();
Map<Integer, Student> studentMap = studentList.stream().collect(Collectors.toMap(Student::getNum, Function.identity()));

Function.identity()函数表示当前对象

将集合分组

与上面的集合转map相对应的就是当你的key不唯一的时候就可以使用groupingBy()方法来实现分组操作。

//模拟一次性批量从数据库查询所有数据
List<Student> studentList = new ArrayList<>();
//按照班级将学生分组
Map<String, List<Student>> studentListMap = studentList.stream().collect(Collectors.groupingBy(Student::getClassName));

利用流排序

对于Student对象,如果一个需求是想要先通过学生的年龄排序,再通过学号排序

//模拟一次性批量从数据库查询所有数据
List<Student> studentList = new ArrayList<>();
studentList.stream().sorted(Comparator.comparing(Student::getAge).thenComparing(Student::getNum)).collect(Collectors.toList());

值得一提的是,Comparator.comparing()是比较器的一种封装,你可以传入所有实现了比较器的对象进行排序,默认是升序。thenComparing可以再前面排序基础上再排序。

当然我们也可以自定义比较器对Student进行排序

studentList.stream().sorted((student1, student2) -> {
    //下面可以根据自己的需求对Student对象指定排序,返回1、-1、0分别代表大于、等于、小于
    if (student1.getAge() > student2.getAge()) {
        return 1;
    } else if (student1.getAge() < student2.getAge()) {
        return -1;
    } else {
        return 0;
    }
}).collect(Collectors.toList());

关于Java中比较器可以看我另一篇文章:关于Java中的比较器

利用流做计算

List<Integer> integerList = new ArrayList<>();
//第一种方法,我们可以将对象流转换为:int、long、double流,然后调用sum()方法求和
int sum = integerList.stream().mapToInt(e -> e).sum();
//如果不用‘int、double、long’基础流的sum()方法,我们可以用reduce()方法来计算
Integer reduce = integerList.stream().reduce(0, (a, b) -> a + b);

reduce()方法是一种聚合函数,它可以通过某种规则将所有流聚合成一个结果

一个容易被忽略的方法

有时候我们需要在处理过程中对流中的数据做一些处理,但又不想要结束流。

比如我想把学生信息先保存到数据库,再计算所有学生的年龄总和

我们就可以利用peek()一步到位,倘若使用forEach(),就会导致流结束。

peek()forEach()唯一区别就是peek()不会结束流

posted @ 2022-06-23 17:18  LovelyLM  阅读(112)  评论(0编辑  收藏  举报