JDK8新特性系列——集合操作神器:Stream API


前言

上篇文章我们已经学习了 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 操作分为三个步骤:

  1. 创建Stream
    我们可以从Collection、数组或者其他数据源里面获取一个Stream
  2. 中间操作
    中间操作是一串操作链,用于对数据源执行处理。由于是操作链,所以肯定是讲究先来后到,并且前一个操作的结果作为下一个操作的输入,这个是重点
  3. 终止操作
    终止操作就表示操作的结束,并且从中间操作中获取结果,并关闭Stream

4.2 Stream 的创建

首先我们来学习如何创建Stream
JDK8中对Collecton接口做了扩展,新增了获取流的方法
在这里插入图片描述

归纳下来的话就是这样的

default Stream<E> stream() // 获取串行流
default Stream<E> parallelStream() // 获取并行流

细心的同学已经发现了,default是什么鬼,为什么接口的方法还有方法体,书上可不是这样教的呀,Jdk8到底发生了什么?

接口的变化后面会讲,我们先把重心放到Stream 上,只要是Collection的实现类都能很方便地调用这两个方法来获得自己的Stream

获取Stream 流的方式

  1. Collection的子类通过stream()或者parallelStream()方法获取流
  2. 使用Arrays的静态方法stream(T[] array)从数组获取流
  3. 使用Stream类的静态方法 of(T t)从任意对象获取流
  4. 使用Streamiterate()generate()创建无限流

在日常开发中使用最多的就是从Collection的子类中获取流,像我们经常使用的ListSet都是Collection的子类。

4.3 Stream中间操作

我们把Stream理解成数据处理工作流的描述,我们刚才学习了如何获取流,那么接下来就是如果处理数据了,所以我们现在学习Stream中间操作。

中间操作是对数据进行某种形式的转换和操作,中间操作会返回新的Stream,进而可以实现中间操作的链式调用。中间操作有一个特点就是延迟执行,这意味着他们不会立即处理数据,而是在终止操作的时候才会真正执行。

中间操作可以根据操作的具体行为分成四类

  1. 筛选和切片
方法描述
filter(Predicate<? super T> )接收Predicate类型的函数式接口,从流中过滤某些元素
distinct()去重,根据对象的hashCode()equals()方法来判断是否重复
limit(long maxSize)截断,保证元素的个数不超过指定数量
skip(long n)跳过元素前n元素,如果长度不够就返回一个空流
  1. 映射
方法描述
map(Function f)把函数f应用到每一个元素上,f的返回值就是新得到的元素
mapToDoubled(ToDoubleFunction f)函数f会作用到每一个元素上,把它们转换成double
返回新的DoubleStream
mapToInt(ToIntFunction f)把元素转换成int,返回新的IntSteram
mapToLong(ToLongFunction f)把元素转换成long,返回新的LongSteram
  1. 排序
方法描述
sorted()采用自然排序的方式,返回一个新的Stream 。自然排序基于Comparable
compareTo 方法,所以流中元素必须实现Comparable接口
sorted(Comparator com)基于比较器排序,返回一个新的Stream对于自定义对象可以采用
这种方式来排序,比实现Comparable更灵活,侵入性更低
  1. 调试
方法描述
peek(Consumer c)查看元素,通常用于调试,切忌用来修改流中元素的属性

有的小伙伴把这三个表格一通看下来,心里面就一个想法:这怎么学得会哦~
我知道你很急,但你你先别急,等我们了解完终止操作 之后写几个案例来消化一下

4.4 Stream终止操作

Stream的终止操作会从调用流的中间操作,生成一个最终结果,调用最终操作之后,流就被消费掉了,不能再进行其他操作。

终止操作也可以根据它的行为分为三类:

  1. 聚合操作
方法描述
count()返回流中元素的数量
max(Comparator<? super T> comparator)根据提供的比较器返回流中的最大元素
min(Comparator<? super T> comparator)根据提供的比较器返回流中的最小元素
sum()返回流中元素的总和(仅适用于原始类型流)
average()返回流中元素的平均值(仅适用于原始类型流)

sumaverage方法没有在Stream流里面,而是在IntStream, LongStream, DoubleStream里面,这三个流是专门处理其对应的 基本类型 的,而Stream流是处理 对象

  1. 查找操作
方法描述
findFirst()返回流中的第一个元素(如果存在)
findAny()返回流中的任意元素(如果存在)
allMatch(Predicate<? super T> predicate)检查流中的所有元素是否满足给定的谓词
anyMatch(Predicate<? super T> predicate)检查流中的任何元素是否满足给定的谓词
noneMatch(Predicate<? super T> predicate)检查流中的元素是否都不满足给定的谓词

map中间操作和reduce终止操作相结合是开发中比较常见的一种形式,用于统计数据,这种使用方式还是受到了Google的Map-Reduce大数据计算模型的启发。

  1. 规约操作
方法描述
reduce(BinaryOperator<T> accumulator)使用给定的累加器函数将流的元素组合起来,返回Optional
reduce(T identity, BinaryOperator<T> accumulator)带有初始值的规约操作
reduce(U identity, BiFunction<U,? super T,U> accumulator, BinaryOperator<U> combiner)更复杂的版本,带有初始值和组合器
  1. 迭代操作
方法描述
forEach(Consumer<? super T> action)对每个元素执行指定的操作
forEachOrdered(Consumer<? super T> action)保证按照流的顺序处理每个元素
  1. 收集操作
方法描述
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名的成绩总和(虽然这个需求很不合理,但是咱们只是为了练习)

先分析一下:

  1. 首先我们从学生对象的List中获取Stream流,然后开始中间操作
  2. 中间操作先使用sorted对学生按照成绩排序产生新的流
  3. 然后使用limit取出前三个产生新的流
  4. 再使用map函数,对学生对象的属性进行映射,取出学生的年龄字段组成新的流
  5. 最后使用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循环来处理数据了,想想都头疼。


那么接下来我们再来完成一个练习:找到班级上成绩最高的学生的学生信息

老规矩,分析一波:

  1. 从学生对象List中创建Stream流
  2. 使用中间操作sorted把学生按照自定义Comparator规则排序,返回新的流
  3. 使用终止操作findFirst来返回第一个对象

其实还有更简单的方法

  1. 从学生对象List中创建Stream流
  2. 直接使用终止操作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);
    }

输出结果如下:
在这里插入图片描述


再来做一个练习:分别统计班级中男生和女生的数量

分析一波:

  1. 从Student对象列表获取Stream流
  2. 使用中间操作filter筛选学生为 的学生对象,并返回新流
  3. 使用终止操作count统计当前流中元素的个数
  4. 女生个数怎么统计,不会又用这个流程走一遍吧?当然不可能,总人数 - 男生个数 就得到了嘛

上代码

    @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其实是非常简单的,大家觉得它比较难可能就是因为它的操作非常灵活,没有一种特定的写法让我们参考,但是这可不是它的缺点,灵活表示它能够处理更多场景下的需求。作为程序员,我们可以先熟悉部分操作,然后加深对其的理解,使用熟练之后就能触类旁通了,用起来有种水到渠成的感觉。

  • 4
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值