Java核心: Stream流的实现原理

Java 8之后我们对Stream的使用都已经习以为常了,它帮助我们从怎么做的细节里脱身,只要告诉它做什么即可。这一篇文章我们主要讲Java Stream的实现原理,手写一个Stream框架,然后再来讲解Java Stream的核心类,做到知其然知其所以然。

我们先看一个Stream简单用例,输入一堆字符串创建流(of),取前5个(limit)、操作每个字符串取首字母(map),最后用终结操作符收集所有的首字母到List中(collect)。

List<Character> data = Stream.of("a", "b", "c", "d", "e", "f", "g").limit(5).map(x -> x.charAt(0)).collect(Collectors.toList());
System.out.println(data);

1. 创建Stream

假设我们要手写一个Stream类,你会怎么实现呢? 要实现Stream.of其实很简单,只要将of提供的参数保存在一个类实例中即可,我们定义了一个ArrayStream来实现这个动作

public static interface MyStream<T> {static <T> MyStream<T> of(T... ts) {return new OfStream<>(ts);}
}public static class ArrayStream<T> implements MyStream<T> {private int fromIdx;private int toIdx;private T[] data;public ArrayStream(T... ts) {this.data = ts;this.fromIdx = 0;this.toIdx = ts.length;}}

2. 支持limit

在ArrayStream的基础上支持limit操作就很容了,只要调整fromIdx、toIdx的值就即可。不过这里我们通过新定义一个类SliceStream来实现,在limit方法中重新创建SliceStream对象。

public static class SliceStream<T> implements MyStream<T> {private T[] data;private int fromIdx;private int toIdx;public SliceStream(T[] data, int fromIdx, int toIdx) {this.data = data;this.fromIdx = fromIdx;this.toIdx = toIdx;}@Overridepublic MyStream<T> limit(int n) {return new SliceStream<T>(this.data, fromIdx, Math.min(fromIdx + n, toIdx));}
}

在ArrayStream的limit也要使用SliceStream的实例。有的同学可能会疑惑为什么不直接用ArrayStream,修改fromIdx/toIdx,这个案例是可以的,新增类只是为了模拟JDK实现的策略。

public static class ArrayStream<T> implements MyStream<T> {...@Overridepublic MyStream<T> limit(int n) {return new SliceStream<>(this.data, 0, Math.min(data.length, n));}...
}

3. 支持map

照着limit的实现如法炮制,我们定义一个MappedStream类,持有转换函数(Funtion),持有下游MyStream,后续读取MyStream时,先用转换函数操作数据。我们来看下代码

public static class MappedStream<T, R> implements MyStream<R> {private Function<T, R> apply;private MyStream<T> stream;public MappedStream(MyStream<T> stream, Function<T, R> apply) {this.stream = stream;this.apply = apply;}...@Overridepublic void forEach(Consumer<R> consumer) {stream.forEach((T t) -> {R r = apply.apply(t);consumer.accept(r);});}
}

在ArrayStream的map方法中,我们讲ArrayStream包装到MappedStream后再返回

public static class ArrayStream<T> implements MyStream<T> {...@Overridepublic <R> MyStream<R> map(Function<T, R> map) {return new MappedStream<>(this, map);}...
}

4. 支持collect

Java Stream讲这类操作称为终结操作,它是实际动作发生的位置。我们定义一个Collector接口,supplier生成中间结果对象,accumulate应用每个元素到中间结果,finisher转换为最终结果

public static interface Collector<T, A, R> {public Supplier<A> supplier();public void accumulate(BiConsumer<T, R> consumer);Function<A, R> finisher();
}

下面是Collector的一个实现,在Collector接口提供一个工厂方法(toList),方便将stream转为list

public static class CollectorImpl<T, A, R> implements Collector<T, A, R> {private Supplier<A> supplier;private BiConsumer<T, A> accumulate;private Function<A, R> finisher;public CollectorImpl(Supplier<A> supplier, BiConsumer<T, A> accumulate, Function<A, R> finisher) {this.supplier = supplier;this.accumulate = accumulate;this.finisher = finisher;}@Overridepublic Supplier<A> supplier() {return supplier;}@Overridepublic BiConsumer<T, A> accumulate() {return accumulate;}@Overridepublic Function<A, R> finisher() {return finisher;}
}public static interface Collector<T, A, R> {static <T> CollectorImpl<T, List<T>, List<T>> toList() {Supplier<List<T>> supplier = ArrayList<T>::new;BiConsumer<T, List<T>> accumulate = (T t, List<T> ls) -> ls.add(t);Function<List<T>, List<T>> finisher = Function.identity();return new CollectorImpl<>(supplier, accumulate, finisher);}
}

在ArrayStream里支持通过collect方法终结操作

public static class ArrayStream<T> implements MyStream<T> {...@Overridepublic <A, W> W collect(Collector<T, A, W> collector) {Supplier<A> supplier = collector.supplier();BiConsumer<T, A> consumer = collector.accumulate();Function<A, W> finisher = collector.finisher();A middle = supplier.get();for (int i = fromIdx; i < toIdx; i++) {T item = data[i];consumer.accept(item, middle);}return finisher.apply(middle);}...
}

测试代码如下,查看输出可以确定我们的实现是正常运作的

public static void main(String[] args) {List<Character> data = MyStream.of("a", "b", "c", "d", "e", "f", "g").limit(5).map(x -> x.charAt(0)).collect(Collector.toList());System.out.println(data);
}

这个手写stream框架的类图是这个样子的,主要涉及了6个类

5. JDK内置实现

上面我们手写了简单的Stream框架,目的是了解它的工作原理。JDK内置大体思路跟这个类似,但要复杂的多。内置实现中也提供了Stream接口,以及Stream接口的各种实现类,本质类似于我们手写框架类的ArrayStream、MappedStream,通过装饰器模式提供额外功能。下图是核心的Stream实现类

我们用一个最简单的案例来看看Java的执行流程

Stream.of(1, 2, 3).filter(i -> i > 2).findAny().ifPresent(System.out::println);

这个调用的时序图如下图,核心步骤包括:

  1. Stream.of调用后,通过StreamSupport.stream创建一个ReferencePipeline.Head类实例
  2. filter(Prediccate)过滤流的时候,会把Head类实例封装为StatelessOp匿名内部类实例,并覆写了onWrapSink方法,在数据提交给Sink之前会先经过Predicate过滤
  3. findAny,创建一个FindOps实例,StatelessOp对象会调用FindOps的evaluateSequentail方法
  4. FindOps的evaluateSequentail方法,调用PipelineHelper类(ReferencePipeline实现该接口)的copyInto方法,copyInto方法内部会检测当前操作是否支持短路
    1. 支持短路操作,调用Spliterator的tryAdvance方法,Sink被设置为cancellationRequested的话就不操作之后的元素
    2. 不支持短路操作,调用Spliterator.forEachRemaining方法

现在我们完全理解Java Stream的运行流程了,再回过头来看看Java Stream中核心概念,Java Stream将操作分为两类,一类是中间操作,一类是终结操作

  • 中间操作,继续返回stream够后续处理
  • 终结操作,返回结果或Optional结束计算

中间操作被分为有状态操作(StatefullOps类)、无状态操作(StatelessOps类),终结操作都以TerminateOp表示,是否短路是通过combinedFlags标记的。Stream上不同API对应不同的操作符。

6. 如何评价Java Stream

有不少的文章/数据于对Java Stream做过性能测试,认为它是低效的。公平的将从实现上来说,Java Stream并没有明显存在严重影响性能的涉及。只是因为它有复杂的调用结构(各种包装器)所以对性能略有影响,而多数测试使用的都是极其简单的CPU密集型计算,所以显得对性能影响很大(有的甚至说比正常迭代慢2倍),如果是正常的业务逻辑完全不需要担心Stream会导致问题,而它带来的可读性是实实在在的。

下一篇我们来说说Java Stream的API支持哪些能力,怎么使用,如何扩展,尽情期待。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/842583.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

vue-3d-loader 加载多个模型

需求 1、在使用three.js进行开发的过程中&#xff0c;需要列表加载多个模型&#xff0c;并根据需要多模型进行加载。 2、当鼠标移动到图片上去的时候&#xff0c;开始加载模型&#xff0c; 模型进行加载和展示。 3、在制作3d沉浸式商城时&#xff0c;需要根据需求&#xff0…

字典推导式

自学python如何成为大佬(目录):https://blog.csdn.net/weixin_67859959/article/details/139049996?spm1001.2014.3001.5501 使用字典推导式可以快速生成一个字典&#xff0c;它的表现形式和列表推导式类似。例如&#xff0c;我们可以使用下面的代码生成一个包含4个随机数的字…

shell编程之面交互

Here Document Here Document使用注意事项 面交互 面交互修改账号密码 [rootlocalhost opt]# passwd zhangsan <<EOF > abc1234 #下面两行是输入密码 > abc1234 > EOF 更改用户 zhangsan 的密码 。 新的 密码&#xff1a;无效的密码&#xff1a;…

贝叶斯:共轭先验(conjugacy)

共轭先验与共轭分布 在贝叶斯统计中&#xff0c;如果后验分布与先验分布属于同类&#xff0c;则先验分布与后验分布被称为共轭分布&#xff0c;而先验分布被称为似然函数的共轭先验。&#xff08;要求后验分布与先验分布是同类分布&#xff0c;不要求似然函数分布相同。&#…

红外超声波雷达测距

文章目录 一HC-SR04介绍1HC-SR04简介及工作原理 二用HAL库实现HC-SR04测量距离1STM32CubeMX配置2keil53代码的添加 三效果 一HC-SR04介绍 1HC-SR04简介及工作原理 超声波是振动频率高于20kHz的机械波。它具有频率高、波长短、绕射现象小、方向性好、能够成为射线而定向传播等…

二叉树尾部分

1.二叉树的销毁 2.二叉树的层序遍历 3.判断二叉树是否为完全二叉树 4.二叉树的性质 1.二叉树的销毁 以后序的方式遍历销毁左右子数&#xff0c;因为前序和中序销毁的话根会被销毁而找不到左右子树的位置&#xff0c;后序的根访问在最后&#xff0c;可以找到左右的子树位置。…

PHP深入理解-PHP架构布局

PHP的架构布局涉及多个层次&#xff0c;让我们一起探讨一下吧&#xff01;&#x1f680; 执行流程&#xff1a;解析为Token&#xff1a;将PHP代码解析成标记&#xff08;tokens&#xff09;。抽象语法树&#xff1a;将语法解析树转换为抽象语法树。Opcodes&#xff1a;将抽象语…

RAG-GPT实践过程中遇到的挑战

引言 前面介绍了使用RAG-GPT和OpenAI快速搭建LangChain官网智能客服。有些场景&#xff0c;用户可能无法通过往外网访问OpenAI等云端LLM服务&#xff0c;或者由于数据隐私等安全问题&#xff0c;需要本地部署大模型。本文将介绍通过RAG-GPT和Ollama搭建智能客服。 RAG技术原理…

强化学习_06_pytorch-PPO2实践(Humanoid-v4)

一、PPO优化 PPO的简介和实践可以看笔者之前的文章 强化学习_06_pytorch-PPO实践(Pendulum-v1) 针对之前的PPO做了主要以下优化&#xff1a; -笔者-PPO笔者-PPO2refdata collectone episodeseveral episode(one batch)activationReLUTanhadv-compute-compute adv as one seri…

CC1链补充-LazyMap

前言 在我们上一篇中详细分析了CC1链&#xff0c;但是在CC1链中还有一条链就是LazyMap类 1.安装和CC1核心 环境安装的详情可以见上篇CC1分析的第二部分&#xff0c;环境搭建部分 两条不同的路线其实第一步核心都是相同的&#xff0c;执行类都是Tansformer接口和实现类&#…

【MySQL事务(上)】

文章目录 前言一、什么是事务&#xff1f;1.关于事务的特性 二、为什么要有事务三、事务的提交方式测试事务准备工作事务的操作1.启动事务2.对事务进行回滚&#xff08;只有在事务进行期间&#xff09;3.提交事务&#xff08;持久化&#xff09;4.事务的异常情况结论 四、事务的…

侧缝计怎么安装_测缝计安装方法介绍

测缝计作为土木工程和结构健康监测中常用的仪器&#xff0c;用于测量裂缝或接缝的张开和闭合情况。正确的安装是确保测缝计能够准确、可靠地工作的关键。本文将详细介绍测缝计的安装方法&#xff0c;以确保测量结果的准确性和可靠性。 上传中 点击输入图片描述&#xff08;最多…

Qt for android 串口库使用

简介 由于Qt for android并没有提供android的串口执行方案&#xff0c;基于需要又懒得自己去造轮子&#xff0c; 使用开源的 usb-serial-for-android 库进行串口访问读写。 如果有自己的需要和库不满足的点&#xff0c;可以查看库的底层调用的Android相关API C/C 串口库 对应…

01Python相关基础学习

Python基础 模块相关导入模块sys模块 模块相关 导入模块 1. import 模块名 2. import 模块名 as 别名 3. from 模块名 import 成员名 as 别名sys模块 1. sys.argv 介绍: 实现从程序的外部想程序传递参数返回的是一个列表,第一个元素是程序文件名,第二个元素是程序外部传入的…

RabbitMQ(一)概述第一个应用程序

文章目录 概述AMQP和JMS官网安装开始第一个程序 概述 消息队列是实现应用程序和应用程序之间通信的中间件产品 AMQP和JMS 工作体系 官网 https://www.rabbitmq.com/ RabbitMQ是一款基于AMQP、由Erlang语言开发的消息队列产品 安装 # 拉取镜像 docker pull rabbitmq:3.13-m…

民国漫画杂志《时代漫画》第7期.PDF

时代漫画07.PDF: https://url03.ctfile.com/f/1779803-1247458105-0a2c41?p9586 (访问密码: 9586) 《时代漫画》的杂志在1934年诞生了&#xff0c;截止1937年6月战争来临被迫停刊共发行了39期。 ps:资源来源网络&#xff01;

Java进阶学习笔记23——API概述

API&#xff1a; API&#xff08;Application Programming Interface&#xff09;应用程序编程接口 就是Java帮我们写好了一些程序&#xff1a;如类、方法等等&#xff0c;我们直接拿过来用就可以解决一些问题。 为什么要学别人写好的程序&#xff1f; 不要重复造轮子。开发…

哈希表详解及模拟实现(unordered_map)

目录 认识哈希表&#xff1a; 哈希冲突&#xff1a; 除留余数法--(常用) 平方取中法--(了解) 折叠法--(了解) 随机数法--(了解) 泛型编程&#xff1a; 闭散列&#xff1a; 线性探测&#xff1a; 二次探测&#xff1a; 扩容&#xff1a; 查找&#xff1a; 插入&#…

PUBG绝地求生卡在初始界面 登不上去 打不开游戏的解决办法

PUBG绝地求生卡在初始界面 登不上去 打不开游戏的解决办法 吃鸡热潮依旧绝地求生PUBG可是咱们玩家的心头好啊&#xff01;不过有时候可能会遇到点小麻烦&#xff0c;比如PUBG绝地求生卡在初始界面 登不上去 打不开游戏的解决办法。小编这就给大家分享几个超实用的解决方法&…

LDRA Testbed(TBrun)软件单元测试_操作指南

系列文章目录 LDRA Testbed软件静态分析_操作指南 LDRA Testbed软件静态分析_自动提取静态分析数据生成文档 LDRA Testbed软件静态分析_Jenkins持续集成_(1)自动进行静态分析的环境搭建 LDRA Testbed软件静态分析_Jenkins持续集成_(2)配置邮件自动发送静态分析结果 LDRA Testb…