前言
上篇文章我们已经学习了 Lambda表达式 和 函数式接口,还不了解的看我上一篇文章
JDK8新特性——函数式接口与Lambda表达式。
相信大家对于这种优雅的写法深有感触,那么今天我们要学习的 Stream API 会大量用到函数式接口和Lambda表达式。
Steam API 是 JDK8 的重磅更新,被誉为集合操作的神器,拯救无数程序员于循环处理集合数据的苦海之中,接下来我将和大家一起来看看Stream API到底有多么好用。
本篇文章对于Stream API 的中间操作和终止操作整理很全面,建议大家收藏起来,有需要随时回来看看。
4. Stream API
4.1 Stream 介绍
Stream 流是JDK8非常强力的一个新特性,可以称之为集合运算的神器,能让程序员写出更高效、简洁的代码。
先想想一个问题,为什么要使用Stream 流?
不严谨的说,Stream流就是为了处理集合而生的。随着技术的发展,我们不再只是从MySQL等关系型数据库查数据,我们也不再只使用SQL来处理数据,我们还需要在内存里面处理数据。
面对这样的需求,Stream 流出现了,并成为了Java集合处理的强有力工具。
Stream 流有一些值得注意的地方:
- Stream 流代表的是数据的操作,本身不会存储数据
- Stream 流不会改变源对象,会返回新的持有新的结果的Stream
- Stream 的操作是延迟执行的,这意味着只有需要结果的时候才会执行
Stream 操作分为三个步骤:
- 创建Stream
我们可以从Collection
、数组或者其他数据源里面获取一个Stream - 中间操作
中间操作是一串操作链,用于对数据源执行处理。由于是操作链,所以肯定是讲究先来后到,并且前一个操作的结果作为下一个操作的输入,这个是重点 - 终止操作
终止操作就表示操作的结束,并且从中间操作中获取结果,并关闭Stream
4.2 Stream 的创建
首先我们来学习如何创建Stream
JDK8中对Collecton
接口做了扩展,新增了获取流的方法
归纳下来的话就是这样的
default Stream<E> stream() // 获取串行流
default Stream<E> parallelStream() // 获取并行流
细心的同学已经发现了,default
是什么鬼,为什么接口的方法还有方法体,书上可不是这样教的呀,Jdk8到底发生了什么?
接口的变化后面会讲,我们先把重心放到Stream 上,只要是Collection
的实现类都能很方便地调用这两个方法来获得自己的Stream
获取Stream 流的方式
- Collection的子类通过
stream()
或者parallelStream()
方法获取流 - 使用
Arrays
的静态方法stream(T[] array)
从数组获取流 - 使用
Stream
类的静态方法of(T t)
从任意对象获取流 - 使用
Stream
的iterate()
和generate()
创建无限流
在日常开发中使用最多的就是从Collection
的子类中获取流,像我们经常使用的List
和Set
都是Collection
的子类。
4.3 Stream中间操作
我们把Stream理解成数据处理工作流的描述,我们刚才学习了如何获取流,那么接下来就是如果处理数据了,所以我们现在学习Stream中间操作。
中间操作是对数据进行某种形式的转换和操作,中间操作会返回新的Stream,进而可以实现中间操作的链式调用。中间操作有一个特点就是延迟执行,这意味着他们不会立即处理数据,而是在终止操作的时候才会真正执行。
中间操作可以根据操作的具体行为分成四类
- 筛选和切片
方法 | 描述 |
---|---|
filter(Predicate<? super T> ) | 接收Predicate 类型的函数式接口,从流中过滤某些元素 |
distinct() | 去重,根据对象的hashCode() 和equals() 方法来判断是否重复 |
limit(long maxSize) | 截断,保证元素的个数不超过指定数量 |
skip(long n) | 跳过元素前n元素,如果长度不够就返回一个空流 |
- 映射
方法 | 描述 |
---|---|
map(Function f) | 把函数f 应用到每一个元素上,f 的返回值就是新得到的元素 |
mapToDoubled(ToDoubleFunction f) | 函数f 会作用到每一个元素上,把它们转换成double 返回新的 DoubleStream |
mapToInt(ToIntFunction f) | 把元素转换成int ,返回新的IntSteram |
mapToLong(ToLongFunction f) | 把元素转换成long ,返回新的LongSteram |
- 排序
方法 | 描述 |
---|---|
sorted() | 采用自然排序的方式,返回一个新的Stream 。自然排序基于Comparable 的compareTo 方法,所以流中元素必须实现Comparable 接口 |
sorted(Comparator com) | 基于比较器排序,返回一个新的Stream对于自定义对象可以采用 这种方式来排序,比实现 Comparable 更灵活,侵入性更低 |
- 调试
方法 | 描述 |
---|---|
peek(Consumer c) | 查看元素,通常用于调试,切忌用来修改流中元素的属性 |
有的小伙伴把这三个表格一通看下来,心里面就一个想法:这怎么学得会哦~
我知道你很急,但你你先别急,等我们了解完终止操作 之后写几个案例来消化一下
4.4 Stream终止操作
Stream的终止操作会从调用流的中间操作,生成一个最终结果,调用最终操作之后,流就被消费掉了,不能再进行其他操作。
终止操作也可以根据它的行为分为三类:
- 聚合操作
方法 | 描述 |
---|---|
count() | 返回流中元素的数量 |
max(Comparator<? super T> comparator) | 根据提供的比较器返回流中的最大元素 |
min(Comparator<? super T> comparator) | 根据提供的比较器返回流中的最小元素 |
sum() | 返回流中元素的总和(仅适用于原始类型流) |
average() | 返回流中元素的平均值(仅适用于原始类型流) |
sum
和average
方法没有在Stream
流里面,而是在IntStream
, LongStream
, DoubleStream
里面,这三个流是专门处理其对应的 基本类型 的,而Stream流是处理 对象 的
- 查找操作
方法 | 描述 |
---|---|
findFirst() | 返回流中的第一个元素(如果存在) |
findAny() | 返回流中的任意元素(如果存在) |
allMatch(Predicate<? super T> predicate) | 检查流中的所有元素是否满足给定的谓词 |
anyMatch(Predicate<? super T> predicate) | 检查流中的任何元素是否满足给定的谓词 |
noneMatch(Predicate<? super T> predicate) | 检查流中的元素是否都不满足给定的谓词 |
map
中间操作和reduce
终止操作相结合是开发中比较常见的一种形式,用于统计数据,这种使用方式还是受到了Google的Map-Reduce大数据计算模型的启发。
- 规约操作
方法 | 描述 |
---|---|
reduce(BinaryOperator<T> accumulator) | 使用给定的累加器函数将流的元素组合起来,返回Optional |
reduce(T identity, BinaryOperator<T> accumulator) | 带有初始值的规约操作 |
reduce(U identity, BiFunction<U,? super T,U> accumulator, BinaryOperator<U> combiner) | 更复杂的版本,带有初始值和组合器 |
- 迭代操作
方法 | 描述 |
---|---|
forEach(Consumer<? super T> action) | 对每个元素执行指定的操作 |
forEachOrdered(Consumer<? super T> action) | 保证按照流的顺序处理每个元素 |
- 收集操作
方法 | 描述 |
---|---|
collect(Collector<? super T,A,R> collector) | 使用给定的 Collector 将元素收集到一个结果容器中 |
collect(Supplier<R> supplier, BiConsumer<R, ? super T> accumulator, BiConsumer<R, R> combiner) | 更通用的收集方法,允许更多的自定义 |
toArray() | 返回一个数组,包含流中的所有元素 |
Collector
接口里面有很多抽象方法,通常我们不需要自己实现它,而是调用Collectors
提供的一些静态方法,这些方法会返回对应的实现类,来对流进行收集。
从各部分的分类来看,Stream流操作是非常简单的,因为它的设计初衷就是简化程序开发,提高效率,分类也就那么几个,大家很容易就能想到一些简单的应用场景。
好了,你已经看到这里了,恭喜你熬过来了,那么接下来我们写几个简单的用例来实际感受下Stream 流的强大之处。
不要求用到所有的操作,重点是体会这个过程,理解工作原理,至于其他操作的组合就看业务的需要和自己的思考了,只有理解了原理才能组合出适合业务的操作链。
4.5 Stream 操作实例
先来最经典的Map-Reduce,现在需要统计一个班的学生前3名的成绩总和(虽然这个需求很不合理,但是咱们只是为了练习)
先分析一下:
- 首先我们从学生对象的List中获取Stream流,然后开始中间操作
- 中间操作先使用
sorted
对学生按照成绩排序产生新的流 - 然后使用
limit
取出前三个产生新的流 - 再使用
map
函数,对学生对象的属性进行映射,取出学生的年龄字段组成新的流 - 最后使用
reduce
终止操作,我们传入0作为初始值,编写自定义累加器来累加成绩
上代码
@Test
public void demo1(){
// 学生集合
List<Student> stuList = StudentGenerator.generate();
// 这里面的函数式接口传入的Lambda表达式部分使用的是方法引用
// 只要是符合函数式接口方法描述的方法(方法名可以不同,入参和返回值必须匹配)都能传入函数式接口
int total = stuList.stream() // 获取流
.sorted((a,b)->b.getScore()-a.getScore()) // 按照学生的成绩降序排序
.limit(3) // 最多取前三个,如果个数不够三个就取所有
.peek(s-> System.out.println("成绩前三的学号有:"+s.getId())) // 使用peek调试,输出看看和自己的预期一样不
.map(Student::getScore) // 使用map映射字段属性
.reduce(0, Integer::sum); // 使用reduce自定义累加器累加每一项的分数
System.out.println("-------我是分割线-------");
System.out.println("班级前三名的成绩总和为:"+total);
}
输出结果如下:
具体详细的代码在Gitee仓库里面,欢迎大家下载调试,要是有帮助的话,记得点个Star。
看了这个示例之后有没有恍然大悟的感觉,原来这么好用,我们想想如果没有Stream API,我们是不是要使用for循环来处理数据了,想想都头疼。
那么接下来我们再来完成一个练习:找到班级上成绩最高的学生的学生信息
老规矩,分析一波:
- 从学生对象List中创建Stream流
- 使用中间操作
sorted
把学生按照自定义Comparator
规则排序,返回新的流 - 使用终止操作
findFirst
来返回第一个对象
其实还有更简单的方法
- 从学生对象List中创建Stream流
- 直接使用终止操作
max
,传入自定义比较规则来获取成绩最高的学生的信息
我们要两种都要实践,真正搞开发讲究的是效率,现在学习讲究的是思维拓展,可以使用多种思路来实现需求
上代码
@Test
public void demo2() {
// 学生数组
List<Student> stuList = StudentGenerator.generate();
// 找出班级成绩最高的学生的信息
Optional<Student> maxScoreStu = stuList.stream() // 获取Stream流
.max(Comparator.comparingInt(Student::getScore)); // 直接使用终止操作 max得到结果
maxScoreStu.ifPresent(System.out::println); // Optional为控制处理提供了更优雅的方式
System.out.println("-------分割线-------");
// 另一种写法
Optional<Student> maxScoreStu1 = stuList.stream()
.sorted((a,b)->b.getScore()-a.getScore())
.findFirst();
maxScoreStu1.ifPresent(System.out::println);
}
输出结果如下:
再来做一个练习:分别统计班级中男生和女生的数量
分析一波:
- 从Student对象列表获取Stream流
- 使用中间操作
filter
筛选学生为 男 的学生对象,并返回新流 - 使用终止操作
count
统计当前流中元素的个数 - 女生个数怎么统计,不会又用这个流程走一遍吧?当然不可能,总人数 - 男生个数 就得到了嘛
上代码
@Test
public void demo3() {
// 学生数组
List<Student> stuList = StudentGenerator.generate();
long boyCount = stuList.stream() // 创建流
.filter(s -> "男".equals(s.getGender()))
.count();
long girlCount = stuList.size() - boyCount;
System.out.printf("男生的数量:%d,女生的数量:%d \n",boyCount,girlCount);
}
运行结果如下:
如果你认真看到这里,并做了练习,那么恭喜你已经入门了Stream流的使用,请在接下来的工作和学习中多多使用它,慢慢摸索一些更多的用法,来实现更复杂的需求。
总结
Stream API其实是非常简单的,大家觉得它比较难可能就是因为它的操作非常灵活,没有一种特定的写法让我们参考,但是这可不是它的缺点,灵活表示它能够处理更多场景下的需求。作为程序员,我们可以先熟悉部分操作,然后加深对其的理解,使用熟练之后就能触类旁通了,用起来有种水到渠成的感觉。