前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >JDK 8 新特性之函数式编程 → Stream API

JDK 8 新特性之函数式编程 → Stream API

作者头像
青石路
发布2020-09-01 17:25:55
4840
发布2020-09-01 17:25:55
举报
文章被收录于专栏:开发技术开发技术

开心一刻

  今天和朋友们去K歌,看着这群年轻人一个个唱的贼嗨,不禁感慨道:年轻真好啊!

  想到自己年轻的时候,那也是拿着麦克风不放的人

  现在的我没那激情了,只喜欢坐在角落里,默默的听着他们唱,就连旁边的妹子都劝我说:大哥别摸了,唱首歌吧

Stream 初体验

  很多时候,我们往往会选择在数据库层面进行数据的过滤、汇聚,这就导致我们对 JDK8 的 Stream 应用的特别少,对它也就特别陌生了

  但有时候,我们可以将原始数据加载到内存,在内存中进行数据的过滤和汇聚,这样可以减少数据库操作,提高查询效率(非绝对,数据量不大或走索引的情况下,数据库查询也是很快的)

  假设我们在内存中进行数据的过滤、汇聚,在 JDK8 之前(或不用 JDK8 的 Stream),我们会如何处理? 多次 for 循环结合 if ,并创建多个集合来存放中间结果,最后对中间结果进行汇聚,代码量会非常大;如果想牛逼一点,用多线程来处理,那就更复杂了(线程池、并发等问题)。Stream 就解决了这些痛点,如果你的 JDK 版本是 8(或更高),你还在用 for 循环进行数据的过滤和汇聚,那就有点这味了

  那 Stream 到底是何方神圣,让楼主如此推崇,我们往下看(再不讲重点,楼主怕是要收刀片了!)

  先闻其声

    我们先来看看她妈是怎么介绍她的: A sequence of elements supporting sequential and parallel aggregate operations.

    我们能从中获取到两个信息:

      1、Stream 是元素的集合(有点类似 Iterator)

      2、对原 Stream 支持顺序或并行的汇聚操作

    这她妈的介绍还是比较抽象,我们需要从 Stream 自身下手,慢慢去了解她

    常见的 Stream 接口继承关系如下

    IntStream, LongStream, DoubleStream 对应的是三种基本类型(int, long, double,不是包装类型),Stream 对应所有剩余类型

    为什么不是这样

    或者取消掉 IntStream, LongStream, DoubleStream,由 Stream 对应所有类型 ?

    我们知道基本类型与包装类型之前的装箱与拆箱是有性能消耗的,频繁的转换会有比较严重的性能损耗,所以为不同数据类型设置不同stream接口,可以提高性能,也可以增加特定接口

  一睹芳容

    上面说了那么多,却始终未一睹 Stream 的芳容,心里着急呀!我们先来瞟一眼

代码语言:javascript
复制
List<Integer> nums = Arrays.asList(1, 8, 0, 5, 3, 2);
// 统计大于 3 的元素个数
long count = nums.stream().filter(e -> e > 3).count();

    是不是很美?千万不要以为 Stream 就这?这还只是她的一条腿,她浑身上下都是宝

    通过上面的简单示例,我们可以剖析出 Stream 的通用语法

    也就是说使用 Stream 基本分三步:创建 Steam、转换Stream、汇聚,下面我们就从这三步详细介绍 Stream

创建 Stream

  Stream 的创建方式有很多,我们只讲最常用的两种

  基于数组: Stream<String> arrayStream = Arrays.stream(new String[]{"123", "abc", "A", "张三"});

  基于 Collection: Stream<String> collectionStream = Arrays.asList("123", "abc", "A", "张三").stream();

  把数组变成 Stream 使用 Arrays.stream() 方法;对于 Collection(List、Set、Queue 等),直接调用 stream() 方法就可以获得 Stream

转换 Stream

  转换 Stream 的目的是对原 Stream 做出某种程度的数据映射/过滤,然后返回一个新的流,交给下一个操作使用,对原 Stream 是没有任何影响的。这类操作都是惰性化的(lazy),就是说,仅仅调用到这类方法,并没有真正开始流的遍历

  由于获取的是一个新的流,而不是我们需要的最终结果,所以 转换 Stream 这个操作有个官方的称呼: Intermediate ,即中间操作

  具体的转换操作有很多,我们挑一些常用的来说明一下

  distinct

    对 Stream 中的元素进行去重操作(去重逻辑依赖元素的 equals 方法),新生成的 Stream 中没有重复的元素

代码语言:javascript
复制
List<Integer> nums = Arrays.asList(1, 2, 1, 3, 2, 5);
Stream<Integer> distinctStream = nums.stream().distinct();

  filter

    对 Stream 中的每个元素使用给定的过滤条件进行过滤操作,新生成的 Stream 只包含符合条件的元素

代码语言:javascript
复制
List<Integer> nums = Arrays.asList(1, 2, 1, 3, 2, 5);
Stream<Integer> filterStream = nums.stream().filter(e -> e >= 2);

  map

    对 Stream 中的每个元素按给定的转换规则进行转换操作,新生成的 Stream 只包含转换生成的元素

代码语言:javascript
复制
List<Integer> nums = Arrays.asList(1, 2, 1, 3, 2, 5);
Stream<String> mapStream = nums.stream().map(e -> e * e + "");

    JDK1.8 还提供了三个专门针对基本数据类型的 map 变种方法:mapToInt,mapToLong 和 mapToDouble。这三个方法也比较好理解,就是把原始 Stream 转换成一个新的 Stream,这个新生成的 Stream 中的元素都是对应的基本类型。之所以会有这三个变种方法,是考虑到自动装箱/拆箱的额外消耗

代码语言:javascript
复制
List<Integer> nums = Arrays.asList(1, 2, 1, 3, 2, 5);
IntStream intStream = nums.stream().mapToInt(e -> e * 2);
LongStream longStream = nums.stream().mapToLong(e -> 3L * e);
DoubleStream doubleStream = nums.stream().mapToDouble(e -> 3.0 * e);

  flatMap

    与 map 类似

代码语言:javascript
复制
<R> Stream<R> map(Function<? super T, ? extends R> mapper);

<R> Stream<R> flatMap(Function<? super T, ? extends Stream<? extends R>> mapper);

    不同的是 flatMap 中每个元素转换得到的是 Stream 对象,然后会把子 Stream 中的元素都放到新的 Stream 中

代码语言:javascript
复制
List<List<String>> groupList = Arrays.asList(Arrays.asList("q","w","e"), Arrays.asList("a", "s", "d"), Arrays.asList("z","x", "c"));
Stream<String> superStarStream = groupList.stream().flatMap(group -> group.stream().map(e -> e + 1));

    简单点理解就是:把几个小的集合中的元素经过处理后合并到一个大的集合中

    类似的,JDK1.8 也提供了三个专门针对基本数据类型的 flatMap 变种方法:flatMapToInt,flatMapToLong 和 flatMapToDouble

  limit

    拷贝原 Stream 中的前 N 个元素到新的 Stream 中,如果原 Stream 中包含的元素个数小于 N,那就获取其所有的元素

代码语言:javascript
复制
List<Integer> nums = Arrays.asList(1, 2, 1, 3, 2, 5);
Stream<Integer> limitStream = nums.stream().limit(4);

  skip

    拷贝原 Stream 除了前 N 个元素后剩下的所有元素到新 Stream,如果原 Stream 中包含的元素个数小于 N,那么返回空 Stream

代码语言:javascript
复制
List<Integer> nums = Arrays.asList(1, 2, 1, 3, 2, 5);
Stream<Integer> skipStream = nums.stream().skip(4);

  sorted

    对原 Stream 进行排序操作,得到一个新的、有序的 Stream

    排序函数有两个,一个是用自然顺序排序,一个是使用自定义比较器排序

代码语言:javascript
复制
Stream<T> sorted();
Stream<T> sorted(Comparator<? super T> comparator);

    使用起来非常简单,如下所示

代码语言:javascript
复制
List<Integer> nums = Arrays.asList(1, 2, 1, 3, 2, 5);
// 自然排序,默认升序排序
Stream<Integer> sortedStream = nums.stream().sorted();
// 自定义排序
Stream<Integer> sortedCompareStream = nums.stream().sorted((a, b) -> b.compareTo(a));

  peek

代码语言:javascript
复制
Stream<T> peek(Consumer<? super T> action);

    生成一个包含原 Stream 所有元素的新 Stream,同时会提供一个消费函数(Consumer 实例),新 Stream 每个元素被消费的时候都会执行给定的消费函数

    与 map 很像,但不会影响新 Stream 中的元素(还是原 Stream 中的元素),可以做一些输出,外部处理等辅助操作

    这个在实际项目中用的不多,知道是怎么回事就好

汇聚

  汇聚操作接受一个 Stream 为输入,反复使用某个汇聚操作,把 Stream 中的元素合并成一个汇总的结果,汇总结果可能是某个值,也可能是一个集合

  汇聚操作能够得到我们需要的最终结果,相当于一个终止操作,所以也有另一个称呼: Terminal ,即结束操作

  一个流只能有一个 terminal 操作,当这个操作执行后,流就被使用“光”了,无法再被操作。所以这必定是流的最后一个操作。Terminal 操作的执行,才会真正开始流的遍历

  JDK1.8 提供了很多常用的汇聚操作,我们一起来看看

  foreach

    这个类似我们平时的 for 循环,遍历 Stream 中的元素,执行指定的操作

代码语言:javascript
复制
List<Integer> nums = Arrays.asList(1, 2, 1, 3, 2, 5);
nums.stream().forEach(num -> System.out.println(num));

  max min count

    作用就是字面意思

代码语言:javascript
复制
List<Integer> nums = Arrays.asList(1, 2, 1, 3, 2, 5);
// 求最大值
Integer max = nums.stream().max(Comparator.naturalOrder()).get();
// 求最小值
Integer min = nums.stream().min(Comparator.naturalOrder()).get();
// 求元素个数
long count = nums.stream().count();
System.out.println("max = " + max);
System.out.println("min = " + min);
System.out.println("count = " + count);

  findFirst

    返回一个 Optional,它包含了 Stream 中的第一个元素,若 Stream 是空的,则返回一个空的 Optional

代码语言:javascript
复制
List<Integer> nums = Arrays.asList(1, 2, 1, 3, 2, 5);
Integer firstNum = nums.stream().findFirst().get();

  findAny

    返回一个 Optional,它包含了 Stream 中的任意一个元素,若 Stream 是空的,则返回一个空的 Optional

代码语言:javascript
复制
List<Integer> nums = Arrays.asList(1, 2, 1, 3, 2, 5);
Integer anyNum = nums.stream().findAny().get();

    在串行的流中,findAny 和 findFirst返回的,都是第一个对象;而在并行的流中,findAny 返回的是最快处理完的那个线程的数据,所以说,在并行操作中,对数据没有顺序上的要求,那么 findAny 的效率会比 findFirst 要快的,但是没有 findFirst 稳定

  anyMatch

    Stream 中是否有任意一个元素满足判断条件,有则返回 true

代码语言:javascript
复制
List<Integer> nums = Arrays.asList(1, 2, 1, 3, 2, 5);
boolean matchResult = nums.stream().anyMatch(num -> num > 2);

  allMatch

    Stream 中所有元素都满足判断条件则返回 true

代码语言:javascript
复制
List<Integer> nums = Arrays.asList(1, 2, 1, 3, 2, 5);
boolean matchResult = nums.stream().allMatch(num -> num > 2);

  noneMatch

    与 allMatch 相反,Stream 中所有元素都不满足判断条件,则返回 true

代码语言:javascript
复制
List<Integer> nums = Arrays.asList(1, 2, 1, 3, 2, 5);
boolean matchResult = nums.stream().noneMatch(num -> num > 7);

  reduce

    reduce 的主要作用是把 Stream 元素组合起来

    它提供一个起始值(种子),然后依照运算规则(BinaryOperator),和 Stream 中的第一个、第二个、第 n 个元素组合,生成一个我们需要的值

    JDK 提供了三种 reduce

代码语言:javascript
复制
T reduce(T identity, BinaryOperator<T> accumulator);

Optional<T> reduce(BinaryOperator<T> accumulator);

<U> U reduce(U identity,
                 BiFunction<U, ? super T, U> accumulator,
                 BinaryOperator<U> combiner);

    参数不同,其返回值类型是有所不同的,但其语义、作用还是一样的

    max()、min()其实都是特殊的 reduce,只是因为它们比较常用,所以就简化书写专门设计出了它们

代码语言:javascript
复制
@Override
public final Optional<P_OUT> max(Comparator<? super P_OUT> comparator) {
    return reduce(BinaryOperator.maxBy(comparator));
}

@Override
public final Optional<P_OUT> min(Comparator<? super P_OUT> comparator) {
    return reduce(BinaryOperator.minBy(comparator));

}

    reduce 在实际项目中用的不多,又非常灵活,我们就简单看几个示例

代码语言:javascript
复制
        List<Integer> nums = Arrays.asList(1, 2, 1, 3, 2, 5);

        // 求和,相当于sum(); 有起始值
        Integer sum1 = nums.stream().reduce(0, Integer::sum);
        Integer sum2 = nums.stream().reduce(0, (a,b) -> a + b);
        // 求和,相当于sum(); 无起始值
        Integer sum3 = nums.stream().reduce(Integer::sum).get();
        System.out.println("sum = " + sum1 + ", sum2 = " + sum2 + ", sum3 = " + sum3);

        // 求最大值,相当于max()
        Integer max = nums.stream().reduce(Integer.MIN_VALUE, Integer::max);
        System.out.println("max = " + max);
        // 求最小值,相当于min()
        Integer min = nums.stream().reduce(Integer.MAX_VALUE, Integer::min);
        System.out.println("min = " + min);

    reduce 擅长的是生成一个值,如果想要从 Stream 生成一个集合或者 Map 等复杂的对象该怎么办呢?就需要 collect 出马了

  collect

    collect 是 Stream 接口中最灵活的,也是最强大的;JDK 中提供了两种 collect

代码语言:javascript
复制
// Supplier supplier是一个工厂函数,用来生成一个新的容器;
// BiConsumer accumulator也是一个函数,用来把Stream中的元素添加到结果容器中
// BiConsumer combiner还是一个函数,用来把中间状态的多个结果容器合并成为一个(并发的时候会用到)
<R> R collect(Supplier<R> supplier,
              BiConsumer<R, ? super T> accumulator,
              BiConsumer<R, R> combiner);

<R, A> R collect(Collector<? super T, A, R> collector);

    我们来各看一个案例

代码语言:javascript
复制
List<String> strList = Arrays.asList("123","abc", "1w1");
String concat  = strList.stream().collect(StringBuilder::new, StringBuilder::append, StringBuilder::append).toString();
System.out.println(concat);

List<String> stringList = strList.stream().collect(Collectors.toList());

    实际应用中,基本上用的是第二种,而且用的是 JDK 中已经提供好的 Collector,在 Collectors 中提供了很多常用的 Collector, 如下

    我们挑一些比较常用的来说明下,有兴趣的可以去通读下

    转集合

      toList、toSet、toMap

代码语言:javascript
复制
public class StreamTest {

    public static void main(String[] args) {
        Person[] personArray = {
                new Person("shangsan", 23), new Person("张三", 23),
                new Person("lisi", 24), new Person("李四", 24),
                new Person("wangwu", 20), new Person("王五", 20)};
        // 转 list
        List<Person> personList = Arrays.stream(personArray).collect(Collectors.toList());
        // 转 set
        Set<Person> personSet = Arrays.stream(personArray).collect(Collectors.toSet());
        // 转 map, key为 name, value 为 Person 实例
        Map<String, Person> personMap = Arrays.stream(personArray).collect(Collectors.toMap(Person::getName, person -> person));

        System.out.println(personList);
        System.out.println(personSet);
        System.out.println(personMap);
    }

    static class Person {
        private String name;
        private Integer age;

        Person(){}

        Person(String name, Integer age) {
            this.name = name;
            this.age = age;
        }

        public String getName() {
            return name;
        }

        public void setName(String name) {
            this.name = name;
        }

        public Integer getAge() {
            return age;
        }

        public void setAge(Integer age) {
            this.age = age;
        }

        @Override
        public String toString() {
            return "Person{" +
                    "name='" + name + '\'' +
                    ", age=" + age +
                    '}';
        }
    }
}

      toMap 有两个注意点

        1、底层调用的是 map.merge 方法,该方法遇到 value 为 null 的情况会报 npe

        2、遇到重复的 key 会直接抛 IllegalStateException,因为未指定冲突合并策略,也就是第三个参数BinaryOperator<U> mergeFunction

    分组

代码语言:javascript
复制
public class StreamTest {

    public static void main(String[] args) {
        Person[] personArray = {
                new Person("shangsan", 23), new Person("张三", 23),
                new Person("lisi", 24), new Person("李四", 24),
                new Person("wangwu", 20), new Person("王五", 20)};

        // 根据年龄进行分组
        Map<Integer, List<Person>> ageGroup = Arrays.stream(personArray).collect(Collectors.groupingBy(Person::getAge));
        System.out.println(ageGroup);
    }

    static class Person {
        private String name;
        private Integer age;

        Person(){}

        Person(String name, Integer age) {
            this.name = name;
            this.age = age;
        }

        public String getName() {
            return name;
        }

        public void setName(String name) {
            this.name = name;
        }

        public Integer getAge() {
            return age;
        }

        public void setAge(Integer age) {
            this.age = age;
        }

        @Override
        public String toString() {
            return "Person{" +
                    "name='" + name + '\'' +
                    ", age=" + age +
                    '}';
        }
    }
}

    求和

代码语言:javascript
复制
// 年龄求和 summingInt、summingLong、summingDouble 类似
Integer ageSum = Arrays.stream(personArray).collect(Collectors.summingInt(Person::getAge));
System.out.println("age sum = " + ageSum);

    求平均值

代码语言:javascript
复制
// 求平均值 averagingInt、averagingLong、averagingDouble 类似
Double averageAge = Arrays.stream(personArray).collect(Collectors.averagingInt(Person::getAge));
System.out.println("average age = " + averageAge);

    其他

代码语言:javascript
复制
// 统计人数
Long count = Arrays.stream(personArray).collect(Collectors.counting());
System.out.println("人数 = " + count);

List<Integer> intList = Arrays.asList(1, 2, 3, 1, 5, 2);
// 求和
Integer sum = intList.stream().collect(Collectors.reducing(0, (a, b) -> a + b));
System.out.println("sum = " + sum);

// 字符串拼接
List<String> strList = Arrays.asList("123", "abc", "666");
String str = strList.stream().collect(Collectors.joining(",", "(", ")"));
System.out.println(str);

并行流

  前面讲了那么多,都是基于顺序流(Stream),JDK1.8 也提供了并行流: parallelStream ,使用起来非常简单,通过 parallelStream() 可能创建并行流,流的操作还是和顺序流一样

代码语言:javascript
复制
List<Integer> intList = Arrays.asList(1, 2, 3, 1, 5, 2);
boolean result = intList.parallelStream().anyMatch(e -> e > 5);
System.out.println("result = " + result);

  顾名思义,并行流可以运用多核特性(forkAndJoin)进行并行处理,从而大幅提高效率,既然能提高效率,为什么实际项目中,顺序流用的更多,而并行流用的非常少了,还是有一些原因的

  1、parallelStream 是线程不安全的

    一旦出现并发问题,大家都懂的,非常头疼

  2、parallelStream 适用于 CPU 密集型任务

    如果 CPU 负载已经很大,还用并行流,不但不会提高效率,反而会降低效率

    并行流不适用于 I/O 密集型任务,很可能会造成 I/O 阻塞

  3、并行流无法保证元素顺序,输出结果具有不确定性

    如果我们的业务需要关注元素先后顺序,那么不能用并行流

  4、lambda 的执行并不是瞬间完成的,所有使用 parallel stream 的程序都有可能成为阻塞程序的源头

总结

  Stream 特点

    无存储:Stream 不是数据结构并不保存数据,它是有关算法和计算的,它只是某种数据源的一个视图,数据源可以是一个数组,Java 容器或 I/O channel

    函数式编程:每次转换,原有 Stream 不改变,返回一个新的 Stream 对象,这就允许对其操作可以像链条一样排列;转换过程可以多次

    惰性执行:Stream 上的转换操作(中间操作)并不会立即执行,只有执行汇聚操作(终止操作)时,转换操作才会执行

    一次消费:Stream 只能被使用一次,一旦遍历过就会失效,就像容器的迭代器那样,想要再次遍历必须重新生成

  Stream 优点

    代码简洁且易理解,这个感受是最明显的,用与不用 Stream,代码量与可阅读性相差甚远

    多核友好,如果想多线程处理,只需要调一下 parallel() 方法,仅此而已

  Stream 操作分类

    分两类:中间操作(Intermediate)、结束操作(Terminal)

    中间操作总是会惰式执行,调用中间操作只会生成一个标记了该操作的新 stream,仅此而已

    结束操作会触发实际计算,计算发生时会把所有中间操作积攒的操作以 pipeline 的方式执行,这样可以减少迭代次数;计算完成之后stream就会失效

  性能问题

    在对于一个 Stream 进行多次转换操作 (Intermediate 操作),每次都对 Stream 的每个元素进行转换,而且是执行多次,这样时间复杂度就是 N(转换次数)个 for 循环里把所有操作都做掉的总和吗?其实不是这样的,转换操作都是 lazy 的,多个转换操作只会在 Terminal 操作的时候融合起来,一次循环完成。我们可以这样简单的理解,Stream 里有个操作函数的集合,每次转换操作就是把转换函数放入这个集合中,在 Terminal 操作的时候循环 Stream 对应的集合,然后对每个元素执行所有的函数

    关于顺序流、并行流、传统 for 的效率问题,大家看看这个:Java Stream API性能测试for循环与串行化、并行化Stream流性能对比

参考

Java 8 中的 Streams API 详解

JDK8函数式编程之Stream API

本文参与 腾讯云自媒体分享计划,分享自作者个人站点/博客。
原始发表:2020-08-31 ,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 开心一刻
  • Stream 初体验
    •   先闻其声
      •   一睹芳容
      • 创建 Stream
      • 转换 Stream
        •   distinct
          •   filter
            •   map
              •   flatMap
                •   limit
                  •   skip
                    •   sorted
                      •   peek
                      • 汇聚
                        •   foreach
                          •   max min count
                            •   findFirst
                              •   findAny
                                •   anyMatch
                                  •   allMatch
                                    •   noneMatch
                                      •   reduce
                                        •   collect
                                        • 并行流
                                        • 总结
                                          •   Stream 特点
                                            •   Stream 优点
                                              •   Stream 操作分类
                                                •   性能问题
                                                • 参考
                                                相关产品与服务
                                                容器服务
                                                腾讯云容器服务(Tencent Kubernetes Engine, TKE)基于原生 kubernetes 提供以容器为核心的、高度可扩展的高性能容器管理服务,覆盖 Serverless、边缘计算、分布式云等多种业务部署场景,业内首创单个集群兼容多种计算节点的容器资源管理模式。同时产品作为云原生 Finops 领先布道者,主导开源项目Crane,全面助力客户实现资源优化、成本控制。
                                                领券
                                                问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档