Stream API

你可能没意识到Java对函数式编程的重视程度,看看Java 8加入函数式编程扩充多少功能就清楚了。Java 8之所以费这么大功夫引入函数式编程,原因有二:

  1. 代码简洁函数式编程写出的代码简洁且意图明确,使用_stream_接口让你从此告别_for_循环。
  2. 多核友好,Java函数式编程使得编写并行程序从未如此简单,你需要的全部就是调用一下parallel()方法。
    对_stream_的操作分为为两类,中间操作(intermediate operations)和结束操作(terminal operations),二者特点是:
  3. 中间操作总是会惰式执行,调用中间操作只会生成一个标记了该操作的新_stream_,仅此而已。
  4. 结束操作会触发实际计算,计算发生时会把所有中间操作积攒的操作以_pipeline_的方式执行,这样可以减少迭代次数。计算完成之后_stream_就会失效。

引用处API教程写得特别好,这里只做一些API记录。

flatMap()

函数原型为` Stream flatMap(Function<? super T,? extends Stream<? extends R>> mapper)``
它可以把几个列表“压平”成一维,非常像pytorch的fatten操作。

Stream<List<Integer>> stream = Stream.of(Arrays.asList(1,2), Arrays.asList(3, 4, 5));
stream.flatMap(list -> list.stream()).forEach(i -> System.out.println(i));

reduce()

从列表中计算导出一个值。如果很抽象,参考reduce系的常见函数 sum(), max(), min(), count()
最长的函数原型:

// identity,初始值
// accumulator,reduce的方式,可以是累加,取更大值等等
// combiner,用于并行计算,多结果合并
<U> U reduce(U identity, BiFunction<U,? super T,U> accumulator, BinaryOperator<U> combiner)

示例:

// 找出最长的单词
Stream<String> stream = Stream.of("I", "love", "you", "too");
Optional<String> longest = stream.reduce((s1, s2) -> s1.length()>=s2.length() ? s1 : s2);
//Optional<String> longest = stream.max((s1, s2) -> s1.length()-s2.length());
System.out.println(longest.get());
// 求单词长度之和
Stream<String> stream = Stream.of("I", "love", "you", "too");
Integer lengthSum = stream.reduce(0, // 初始值 // (1)(sum, str) -> sum+str.length(), // 累加器 // (2)(a, b) -> a+b); // 部分和拼接器,并行执行时才会用到 // (3)
// int lengthSum = stream.mapToInt(str -> str.length()).sum();
System.out.println(lengthSum);

collect()

reduce是导出一个值,试想我们想导出一个集合,那该怎么办?这就需要使用collect。
导出集合至少需要声明两件事:

  1. 集合是什么?
  2. 如何添加元素?

这刚好就是collect的API完成的内容。

导出集合

// 函数原型
// supplier,提供目标集合获取方式
// accumulator,声明消费方式,即添加方式
// combiner,并行计算,多结果结合方式
<R> R collect(Supplier<R> supplier, BiConsumer<R,? super T> accumulator, BiConsumer<R,R> combiner)// 将Stream规约成List
Stream<String> stream = Stream.of("I", "love", "you", "too");
List<String> list = stream.collect(ArrayList::new, ArrayList::add, ArrayList::addAll);// 方式1
//List<String> list = stream.collect(Collectors.toList());// 方式2
System.out.println(list);// 由于使用Collectors.toList() 无法指定列表类型
// 所以可以使用toCollection()指定规约容器的类型
ArrayList<String> arrayList = stream.collect(Collectors.toCollection(ArrayList::new));// (3)
HashSet<String> hashSet = stream.collect(Collectors.toCollection(HashSet::new));// (4)

生成map和分组统计

生成map比较复杂一些,需要告知如何生成key和value。在这个基础上,stream的API还可以完成一些类似SQL聚类的强大操作。

// map生成
// 使用toMap()统计学生GPA
Map<Student, Double> studentToGPA =students.stream().collect(Collectors.toMap(Function.identity(),// 如何生成keystudent -> computeGPA(student)));// 如何生成value// 二分区
// Partition students into passing and failing
Map<Boolean, List<Student>> passingFailing = students.stream().collect(Collectors.partitioningBy(s -> s.getGrade() >= PASS_THRESHOLD));// 分组
// Group employees by department
Map<Department, List<Employee>> byDept = employees.stream().collect(Collectors.groupingBy(Employee::getDepartment));// 分组统计
// 使用下游收集器统计每个部门的人数
Map<Department, Integer> totalByDept = employees.stream().collect(Collectors.groupingBy(Employee::getDepartment,Collectors.counting()));// 下游收集器// 按照部门对员工分布组,并只保留员工的名字
Map<Department, List<String>> byDept = employees.stream().collect(Collectors.groupingBy(Employee::getDepartment,Collectors.mapping(Employee::getName,// 下游收集器Collectors.toList())));// 更下游的收集器

处理字符串

python的字符串处理一直让其他语言比较羡慕,collect可以完成部分功能,例如join。

// 使用Collectors.joining()拼接字符串
Stream<String> stream = Stream.of("I", "love", "you");
//String joined = stream.collect(Collectors.joining());// "Iloveyou"
//String joined = stream.collect(Collectors.joining(","));// "I,love,you"
String joined = stream.collect(Collectors.joining(",", "{", "}"));// "{I,love,you}"

引用

https://github.com/CarpenterLee/JavaLambdaInternals/blob/master/4-Streams%20API(I).md
https://github.com/CarpenterLee/JavaLambdaInternals/blob/master/5-Streams%20API(II).md

Stream Pipelines 实现原理

Stream API用起来非常的舒服,以至于我们巴不得将所有的迭代换成stream的形式,而完全不想理可读性问题。而简洁的实现下面似乎隐藏的无尽的秘密,反正对于我来说,从接触stream API开始,我至少有以下的疑惑:

  • 流式编程、函数式编程明显是由接口之类的组合实现的,但具体的如何实现的呢?
  • stream是否会有效率方面的问题,例如filter(word -> word.length() < 4).map(String::length),显然包含了两种不同的迭代操作。如果是我来实现,为了效率,我肯定会在一个迭代中去实现多个功能,那么,对于stream API,它是否足够智能,能在一个迭代中完成两种操作,保证效率的问题?

比如下边的一个stream实现的低效版本——在每个阶段都分开地用迭代完成。

stream API的源码不太好读,它属于那种包含着大量的设计的源码,阅读之前,必须先理解整个的框架,以及常见的概念,类都是干嘛的。

操作分类

Stream上的所有操作分为两类:中间操作和结束操作,中间操作只是一种标记,只有结束操作才会触发实际计算。中间操作又可以分为无状态的(Stateless)和有状态的(Stateful),无状态中间操作是指元素的处理不受前面元素的影响,而有状态的中间操作必须等到所有元素处理之后才知道最终结果,比如排序是有状态操作,在读取所有元素之前并不能确定排序结果;结束操作又可以分为短路操作和非短路操作,短路操作是指不用处理全部元素就可以返回结果,比如_找到第一个满足条件的元素_。之所以要进行如此精细的划分,是因为底层对每一种情况的处理方式不同。 为了更好的理解流的中间操作和终端操作,可以通过下面的两段代码来看他们的执行过程。
ref: https://github.com/CarpenterLee/JavaLambdaInternals/blob/master/6-Stream%20Pipelines.md

Stream操作分类
中间操作(Intermediate operations) 无状态(Stateless) unordered() filter() map() mapToInt() mapToLong() mapToDouble() flatMap() flatMapToInt() flatMapToLong() flatMapToDouble() peek()
有状态(Stateful) distinct() sorted() sorted() limit() skip()
结束操作(Terminal operations) 非短路操作 forEach() forEachOrdered() toArray() reduce() collect() max() min() count()
短路操作(short-circuiting) anyMatch() allMatch() noneMatch() findFirst() findAny()

操作分类底层分开处理,这对于底层的实现和设计是非常重要的,不过,实际上对于我们理解stream API的最重要的原理的实现,并没有太多帮助,这边可以先做了解。

Pipelines 双向调用链

目前能看到的教程,对stream实现原理的介绍都比较复杂,但实际上我理解的,核心的设计其实挺好理解的。

核心的原理就是上图,当前其中会有大量的不认识的名词,这些都不重要,只需要知道每一个节点都是代表一个stage,也可以称作一个pipeline节点。

  • stream的API的调用都会生成一个stage,这些stage以双向链表的形式连起来,尽管底层实现并不是直观的双向链表。
  • 我们需要的各种操作,通过lambda函数、接口组合的形式被每个stage持有,这些操作形成操作链。
  • stream企图通过一次迭代完成我们标记的所有操作,这个迭代中,每次都会调用完整的操作链。

上述说明隐藏了大量细节,但事实整体的原理就是这么回事,其实非常好理解:

  • pipline节点和操作串成双向链表
  • 在一次迭代中执行所有操作

接下来是一些细节问题,首先是图中涉及到的类实际上都是pipeline的一种,如下图。具体而言,Head(头节点),StatelessOp(无状态操作),StatefulOp(有状态操作),都是继承自pipeline,你可以直接把它们都理解成pipeline的节点。每个Pipline中持有upStream(也叫previousStage)的引用,从而连成本pipelines链表。

其次操作的串联,操作是通过匿名函数,封装在接口中,最后包装在Sink类中。每个pipeline节点都包含一个Sink,每个Sink包含一个Sink的域,持有下一个Pipline的Sink对象的引用。
从而,Pipeline和Sink构成了宏观上的双向链表。

另外一个细节是Sink链的构建,Sink包装成链是在终结操作调用之后通过Pipeline链,逆向便利包装的,它的代码大致如下,非常好理解。我们可以看到,sink一层套一层,最后返回的sink,实际上就是Pipeline链头节点的Sink操作,从它开始,引用了整个链上的所有sink。这也是它的命名的原因——仿佛是一个水槽,沉淀了整个链上的所有操作,当然这是我自己的理解。

final <P_IN> Sink<P_IN> wrapSink(Sink<E_OUT> sink) {Objects.requireNonNull(sink);for ( @SuppressWarnings("rawtypes") AbstractPipeline p=AbstractPipeline.this; p.depth > 0; p=p.previousStage) {sink = p.opWrapSink(p.previousStage.combinedFlags, sink);}return (Sink<P_IN>) sink;
}

最后是stream执行的时候的调用,也非常好理解,Sink继承自Consumer,所以action看成Sink对象就可以。

public void forEachRemaining(Consumer<? super T> action) {Object[] a; int i, hi; // hoist accesses and checks from loopif (action == null)throw new NullPointerException();// a保存了stream的数据列表,hi是遍历的边界,通常等于数据列表长度if ((a = array).length >= (hi = fence) &&(i = index) >= 0 && i < (index = hi)) {do { // 因为继承自Consumer,通过调用accept可以调用我们的lambda实现,一层一层调用Sinkaction.accept((T)a[i]); } while (++i < hi);}
}

引用

原文来自https://github.com/CarpenterLee/JavaLambdaInternals/blob/master/6-Stream%20Pipelines.md,这一系列文章质量很高,本文算是我自己学习后的一些总结。

Java Stream 底层原理相关推荐

  1. java底层原理书籍_不愧是阿里p8大佬!终于把Java 虚拟机底层原理讲清楚了,请签收...

    概述 JVM 的内存模型和 JVM 的垃圾回收机制一直是 Java 业内从业者绕不开的话题(实际调优.面试)JVM是java中很重要的一块知识,也是面试常问的问题之一,直至今天,仍然还有许多面试者在被 ...

  2. Java CAS底层原理

    Java CAS底层原理 Java CAS底层原理,这一篇就够了!!! CAS全称(Conmpare And Swap)比较并交换,是一种用于在多线程环境下实现同步功能的机制.CAS 操作包含三个操作 ...

  3. 10分钟看懂, Java NIO 底层原理

    点击上方 好好学java ,选择 星标 公众号 重磅资讯.干货,第一时间送达 今日推荐:硬刚一周,3W字总结,一年的经验告诉你如何准备校招! 个人原创100W+访问量博客:点击前往,查看更多 写在前面 ...

  4. Java NIO 底层原理详解

    写在前面 很多的小伙伴,被java IO 模型,搞得有点儿晕,一会儿是4种模型,一会儿又变成了5种模型. 很多的小伙伴,也被nio这个名词搞晕了,一会儿java 的nio 不叫 非阻塞io,一会儿ja ...

  5. 10分钟看懂 Java NIO 底层原理

    写在前面 很多的小伙伴,被java IO 模型,搞得有点儿晕,一会儿是4种模型,一会儿又变成了5种模型. 很多的小伙伴,也被nio这个名词搞晕了,一会儿java 的nio 不叫 非阻塞io,一会儿ja ...

  6. 面试突然问Java多线程底层原理,我哭了!

    兄弟们,不要踩坑啊,我原本打算在金九银十之前换份工作,结果出去第一面就被干懵了! 面试官上来就问我了解不了解多线程,我感觉我还可以,我就和他说:必须的! 不过,他直接问了多线程的底层原理,这我都是一知 ...

  7. java i 底层原理,《Java基础知识》Java Hash底层原理

    前言 了解到JDK8对HashMap进行了优化,就一起了解一下JDK8的HashMap. 原理 1. 哈希表的原理 首先需要一张Hash表,Java通过数据实现:默认长度位16,并且. 第一步插入张三 ...

  8. Java集合底层原理理解

    Java集合 List,Set,Map三者区别 List 顺序的好帮手:存储一组不唯一的有序的对象 Set 注重独一无二的性质:不允许重复的集合 Map 用key来搜索:使用键值对存储.两个key可以 ...

  9. java web底层原理_详解Java开发Web应用程序的底层原理

    前言 前面一篇文章,我从整个应用程序的整体以及跟运行环境的关系简单聊了一下我们现在常用的Spring框架的设计基础和准则,其中主要是控制反转和依赖注入,以及容器化编程等概念. 这里我不想去复述这些概念 ...

最新文章

  1. tf.keras.losses.MeanAbsoluteError 损失函数 示例
  2. 为Zepto添加Slide动画效果
  3. 好东西仿Vista窗体
  4. Windows环境下搭建Tomcat
  5. opencv python3树莓派_树莓派4B日志七:Python3上的OpenCV安装
  6. 数据结构实验病毒感染检测问题(C++)
  7. Redis 3.0正式版发布,正式支持Redis集群
  8. GCJ-02转WGS84
  9. python版CSDN博客备份工具
  10. 1024程序员节是怎么来的?为什么设置程序员节?
  11. QQ安装时注册组件不动的解决方法
  12. Java中的BigDecimal,你真的会用吗?
  13. 一心多用多线程-细谈java线程池submit与execute的区别
  14. c语言实现hdr图像合成,怎样完成一张HDR照片的拍摄与合成
  15. 请求一次服务器会显示请求了两次,在Appengin中,每个请求都会到达服务器两次...
  16. 2022 前端一场面试及答案整理
  17. Java入门学习-实践:手动输入城市名称5次,完成后输出一份city.txt文档,写出被输入最多次的城市名字
  18. Android下的配置管理之道之OpenGrok代码索引环境搭建
  19. 【教程】yolov5_deepsort目标跟踪算法检测人流量计数
  20. 阐述《文明》之父Sid Meier的游戏人生

热门文章

  1. 日、周、月活跃用户数,用户流失率
  2. 解决idea热部署问题
  3. 解决18.04 ubuntu的触控板右键失灵
  4. mysql聚合函数求和
  5. 运维人员的职业升级道路
  6. GitHub仓库统计工具
  7. Android 开源框架ImageLoader的完美例子
  8. 一文透彻了解缺页异常
  9. LeetCode(C++)刷题计划:15-三数之和
  10. 供热管道又称为PERTII型供热管道