本文已参与好文召集令活动,点击查看:后端、大前端双赛道投稿,2万元奖池等你挑战!
首先分享之前的所有文章 , 欢迎点赞收藏转发三连下次一定 >>>> 😜😜😜
文章合集 : 🎁 juejin.cn/post/694164…
Github : 👉 github.com/black-ant
CASE 备份 : 👉 gitee.com/antblack/ca…
一 . 前言
这一篇从函数式接口一步步说起 , 来聊一聊 Java 8 中这个特性的原理以及优点
函数式编程之前一直是基于使用 , 最近梳理源码的时候 ,发现这个概念真的无处不在 , 索性把这一块完整的梳理处理
Java Stream 的基本使用可以看这一篇 :操作手册 : Stream 流处理手册
本篇文章会分为 2个主体 :
- Java 的函数式编程
- Java 的 Stream 原理
二 . 函数式编程原理
函数式编程主要有四种接口 : Consumer、Supplier、Predicate、Function , 每个函数接口都有一个单独的抽象方法
注解: 函数式接口通过 @FunctionalInterface 注解进行标注 , 该注解只能标注在 有且只有一个抽象方法 的接口. (PS:函数式接口不一定非要加该注解)
1 | java复制代码// 以 Consumer 为例 , 可以看到 , 这里的唯一抽象方法是 accept |
这里有几个概念 :
- 函数式编程接口中只能有一个抽象方法
- 可以有 static 和 default 方法 (PS : 不属于抽象方法)
- 可以重写 Object 方法 (PS : 函数本身继承于 Object , 这里后面会执行看看)
- 注解非必须
2.1 函数式编程的使用
先来自定义一个函数式编程的流程 , 了解一下其细节 :
2.1.1 箭头函数Demo
1 | java复制代码// 来看一下函数式编程的简单使用 : |
2.1.2 双冒号函数Demo
1 | java复制代码public void testDoubleFunction() { |
2.2 相关注解 / 接口
上面看完了函数式编程的自定义方式 , 这里来看一下相关的接口 👉
Java 已知的函数式接口有四个主要的 : Consumer、Supplier、Predicate、Function , 以及其他类似的 : IntConsumer , IntSupplier , UnaryOperator 等等 , 这里我们只要就 4 种主要的进行一个简单的分析
Consumer : 消费接口
理解 : 该接口函数的目的是为了消费传入的参数 , 主要集中在参数的使用上结构 : 从结构上就可以看出其特性 ,它是接收一个参数 ,但是没有返回 ( void )
1 | java复制代码public interface Consumer<T> { |
Supplier : 供给型
理解 : Supplier 的作用场景主要单纯的获取资源结构 : 没有输入 ,只有返回
1 | java复制代码public interface Supplier<T> { |
Predicate : 断言型(谓词型)
理解 : 断言型表示一个参数的断言(布尔值函数 , PS : 文档里面经常说谓词 ,但是我感觉翻译为断言更符合)
结构 : 传入对象 ,返回布尔
1 | java复制代码public interface Predicate<T> { |
Function : 功能型
理解 : 接受一个参数并产生一个结果的函数 , 也是功能适用性最好的方式
1 | java复制代码public interface Function<T, R> { |
Consumer 与 IntConsumer 类似接口的区别 , 以及优化思考
可以看到 , 其中每一个函数接口中都为其手动扩展了一些基本类型的接口 , 例如 IntConsumer 等等
之前从资料中了解到 , 这样的目的是为了对基本类型进行优化 , 但是看源码的时候并没有直接的区别 :
个人考虑了一下 , 泛型主要是编译器进行处理 , 在实际的使用阶段是没什么影响的 , 而 Java 基本类型的包装功能实际上也没什么优化作用 , 那么这里到底优化了什么呢 ?还是说单纯的为了更清晰 ?
// PS : 我实在不相信 JDK 里面会做这种事 , 所以 , 一定还有位置!!!
联想一下 , 在编译器之前就确定实际类型 , 那么一定在业务代码中有直观的地方去处理这种类型 , 而避免使用反射等方式在运行时处理类型.
TODO : 挺有意思的 , 不过以后再看看 …
2.2 方法函数原理
前面说了函数式编程的用法 , 现在进入正题 , 看一看函数式编程的原理 :
箭头函数的使用 :
1 | java复制代码// 从自定义的案例来分析 , 传入的实际上是一个接口对象 |
三 . Steam 深入
3.1 Stream 体系结构
从图里面可以看到 , 基本上体系得结构都是一致的 , 有点像树状结构 :
第一层是基础抽象类 : BaseStream
第二层是抽象层次 , 包含5个只要类 : AbstractPipeline / Stream / IntStream / DoubleStream / LongStream
第三层是实现主类 : DoublePipeline / LongPipeline / IntPipeline / ReferencePipeline
第四层为内部类 , 每个实现都有对应的几个 : Head / StatelessOp / StatefulOp / OfInt
3.2 Stream 运行原理
可以看到 , Stream 结构中 , 主要基于 Pipeline 的概念 , 其中额外对三个基本类型做了优化 .
同时通过 StatefulOp、StatelessOp用于对应有状态和无状态中间操作 , 做一个简单的概念整理 :
流程图解 (一图带你了解主流程)
前置补充 : AbstractPipeline 类
AbstractPipeline 的作用 :
stage 是一种虚拟概念 , AbstractPipeline表示流管道的初始部分,封装了流源和零个或多个中间操作 , 一个 AbstractPipeline 被看成一个 stage , 其中每个阶段要么描述流源,要么描述中间操作.
stage 属性 :
AbstractPipeline 中有三个 stage 概念 (用于标注空间结构)
F- AbstractPipeline sourceStage : 指向管道链的头部(如果这是源阶段,则为self)
F- AbstractPipeline previousStage : “上游” 管道,如果这是源级,则为空
F- AbstractPipeline nextStage : 管道中的下一个阶段,如果这是最后一个阶段,则为空
上面三个属性是用于标注空间结构 , 意味着流程走到了哪里 , 剩下的就是如何标注行为
行为类型 :
Head 、StatefulOp 、StatelessOp , 这三个属性都继承了 AbstractPipeline , 同时他们标识了三种操作类型
Head : 表示第一个Stage,也就是source stage , 主要是资源收集
StatefulOp : 有状态操作
StatelessOp : 无状态操作
通常执行逻辑 : Head -> StatefulOp -> StatelessOp
其他属性 :
1 | java复制代码// 上游管道,第一次创建流则为null |
AbstractPipeline 构造函数
1 | java复制代码AbstractPipeline(Supplier<? extends Spliterator<?>> source, |
3.2.1 Stream 的流程
前置要点
这里梳理了一下从相关博客中了解到的 Stream 操作的全部要点 , 便于后文学习 :
1 | java复制代码// 主流程 |
使用案例
1 | java复制代码// 按照以下案例来看一下主要流程 : |
3.2.1.1 Step 1 : Stream 的创建
通常可以通过 Collection 或者 Array 实现 Stream 的创建 , 这里我们不关注太多的细节 , 只是看一下创建出来的是什么样的
- 通过 Spliterator 切割集合 (集合或者数组本身的方法)
- 通过 StreamSupport.stream 构建一个 Stream
1 | java复制代码// 可以看到 , 通过 ReferencePipeline.Head 构建了一个 Stream |
3.2.1.2 Step 2 : Filter 过滤
前面看了 Stream 的创建流程 :
1 | java复制代码public final Stream<P_OUT> filter(Predicate<? super P_OUT> predicate) { |
这一段代码里面集中说了什么 :
- 构建了一个 StatelessOp
- Sink.ChainedReference 是一个反向回调逻辑 ,在流 foreach 的时候 , 这个操作才会执行
补充 : 核心操作 wrapSink
1 | java复制代码// 作用 : |
着重分析一下这个环节 :
什么是管道 , 管道就是一个队列 , 一头进水 , 一头才能出水
而filter 相当于其中的分流阀门 , 把一部分水分出去 , 但是一切的前提就是 , 管道的开关要打开
也就是说当流程走到 foreach 等 Terminal 操作的时候 , 流才开始运行 , 其中设置的中间操作才会执行
3.2.1.3 Step 3 : forEach 流程
forEach 入口
1 | java复制代码C- ReferencePipeline (java.util.stream.SortedOps$OfRef) |
内部流程第一步 : wrapSink 构建 Sink
1 | java复制代码public <S> Void evaluateSequential(PipelineHelper<T> helper,Spliterator<S> spliterator) { |
内部流程第二步 : 发起 Foreach 循环
1 | JAVA复制代码public void end() { |
内部流程第三步 : foreach 主流程
1 | JAVA复制代码public void forEach(Consumer<? super E> action) { |
内部流程第四步 : 执行 accept
1 | JAVA复制代码static final class OfRef<T> extends ForEachOp<T> { |
3.2.1.4 补充 Sink :
作用 :
- begin(long size) : 开始遍历元素之前调用该方法,通知Sink做好准备
- end() : 所有元素遍历完成之后调用,通知Sink没有更多的元素了
- cancellationRequested() : 是否可以结束操作,可以让短路操作尽早结束 (短路操作必须实现)
- accept(T t) : 遍历元素时调用,接受一个待处理元素,并对元素进行处理 (PS : 链表中的 Stage 通过 accept 下层调用)
1 | java复制代码// 调用流程 : |
四 . 要点补充
4.1 Map 流程
1 | JAVA复制代码// |
4.2 Collection 流程
1 | java复制代码public final <R, A> R collect(Collector<? super P_OUT, A, R> collector) { |
4.3 stateless 和 stateful 处理的区别
- stateless : 无状态操作
- stateful : 有状态操作
1 | java复制代码 abstract static class StatelessOp<E_IN, E_OUT> |
4.4 并行处理
1 | java复制代码// 前面已经了解到创建 evaluate 时 , 会通过 evaluateParallel 进行并行操作 |
1 | java复制代码public void compute() { |
这里可以看一下这位前辈的文章 , 写的很清楚 , 以下是搬运的图片 @ Java8 Stream原理深度解析 - 知乎 (zhihu.com)
4.5 Op 模块体系
1 | java复制代码// PS : 这里每一个 Op 都会创建一个 Sink |
4.6 多个 stage 处理
- 通过 构造器设置
- depth 用于设置深度
其他结构如图所示
总结
Stream 原理并没有深入太多 , 主要是对其原理有一定的好奇 ,只对一个流程进行了分析.
Stream 的源码读的很爽 , 很少看到结构这么有趣的代码.
此处还没有完全分析清楚 Stream 是如何通过并行去实现高效处理的 , 下一篇我们看一下性能分析
思考
花了这么久的时间 , 梳理完了这些源码 , 总要从里面学到点什么 , 这里试着做一点总结 :
- Stream 的流程非常有趣 , 它是一种类似于递归但是又略有不同的结构 , 底层逻辑是开关 , 用于开启整个流程 , 当水流动的时候 , 还是从头开始执行
- Stream 的结构体系也很有意思 , 有点类型于链表的 Node next and pre , 只不过其中是虚拟的 stage 对象
- 在继承体系上 , 第一感觉是整整齐齐 , 而且很细致 , 在接口构造上 , 很有参考的价值
附录
Stream 常见方法 :
参考
@ 深入理解Java8中Stream的实现原理_lcgoing的博客-CSDN博客_stream原理
@ Java8 Stream原理深度解析 - 知乎 (zhihu.com)
@ www.javabrahman.com/java-8/unde…)
本文转载自: 掘金