Java核心: Stream流的实现原理

news2024/12/28 8:24:38

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;
    }

    @Override
    public 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> {
    ...
    @Override
    public 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;
    }

    ...
    @Override
    public 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> {
    ...
    @Override
    public <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;
    }

    @Override
    public Supplier<A> supplier() {
        return supplier;
    }

    @Override
    public BiConsumer<T, A> accumulate() {
        return accumulate;
    }

    @Override
    public 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> {
    ...
    @Override
    public <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.coloradmin.cn/o/1709572.html

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

相关文章

一分钟学习数据安全——数字身份的三种模式

微软首席身份架构师金卡梅隆曾说&#xff1a;互联网的构建缺少一个身份层。互联网的构建方式让你无法得知所连接的人和物是什么。这限制了我们对互联网的使用&#xff0c;并让我们面临越来越多的危险。如果我们坐视不管&#xff0c;将面临迅速激增的盗窃和欺诈事件&#xff0c;…

富唯智能镀膜上下料设备采用最新的技术

现代工业竞争日趋激烈&#xff0c;高效生产已成为企业持续发展的关键。我们的设备不仅实现了高速上下料&#xff0c;更通过智能化控制系统实现了对生产流程的精准监控和调整&#xff0c;轻松应对高强度生产需求。 1、快速响应&#xff0c;高效生产 富唯智能镀膜上下料设备采用…

计算机网络学习笔记——网络层(b站)

目录 网络层概述 网络层提供的两种服务 ①面向连接的虚电路服务 ②无连接的数据报服务 IPv4 路由选择 路由器转发IP数据报 静态路由选择 动态路由选择 路由信息协议RIP 开放最短路径优先OSPF&#xff08;Open Shortest Path First&#xff09; 内部网关协议IGP&…

TypeScript系列之-- 数组和元组类型

数组的定义&#xff1a; 第一种&#xff0c;可以在元素类型后面接上[] let list: number[] [1, 2, 3]; 第二种方式是使用数组泛型&#xff0c;Array<元素类型> let list: Array<number> [1, 2, 3]; 如果数组想每一项放入不同数据怎么办&#xff1f;用元组类型…

B+树和B*树

B树和B*树 一、B树的简单介绍二、B树的插入过程三、B*树的简单介绍四、B树、B树、B*树总结五、B树的应用1、MyISAM索引实现2、InnoDB索引实现 一、B树的简单介绍 B树是B树的变形&#xff0c;是在B树基础上优化的多路平衡搜索树&#xff0c;B树的规则跟B树基本类似&#xff0c;但…

order by工作过程和优化

工作过程 order by 是由优化器决定的&#xff0c;如果优化器认为filesort速度快&#xff0c;那么走filesort排序&#xff0c;如果优化器认为索引速度快&#xff0c;那么走索引排序。

Sui新共识协议刷新了区块链交易速度的标准

Sui是提供业界领先性能和无限水平扩展的创新Layer 1区块链&#xff0c;今日在官推上宣布其最新共识协议Mysticeti已成功部署到测试网。这一重大突破将Sui测试网的共识时间减少了80%&#xff0c;至390毫秒&#xff0c;同时保持协议的行业领先吞吐量。这一令人印象深刻的演示证明…

SPP/BLE蓝牙双模方案,主从一体,串口速率可达85KB/S

MS-BTD020A是一款蓝牙5.0双模数传模块&#xff0c;支持SPP&#xff08;经典蓝牙&#xff09;和BLE&#xff08;低功耗蓝牙&#xff09;。蓝牙双模技术使其能够在传统蓝牙和低功耗蓝牙之间无缝切换&#xff0c;用户只需要进行简单的设置就可以实现串口与手机之间的无线传输。模块…

Typora图床配置优化(PicGo-Core(command line) 插件 + gitee)

Typora图床配置优化&#xff08;PicGo-Core(command line) 插件 gitee&#xff09; 前言 在日常使用Typora编写markdown笔记时&#xff0c;经常需要插入图片来帮助理解和整理逻辑。然而&#xff0c;由于图片保存在本地&#xff0c;上传到网上时经常出现图片不见或错误警告的…

Spring和Mybatis的整合

一、需要引入Mybatis 和Spring 整合需要的jar mybatis需要的jar&#xff1a; <!--引入工程需要的jar--><!-- 实体注解--><dependency><groupId>org.projectlombok</groupId><artifactId>lombok</artifactId><version>1.18.24&…

Clickhouse字典关联外部 MySQL 表联合查询实践

前言 clickhouse 可以将源数据加载进 clickhouse 作为字典表使用&#xff0c;字典表可以理解为 clickhouse 中的一张特殊表&#xff0c;我们在查询 clickhouse 表中的数据的时候不需要 JOIN 就可以直接查询字典表中的数据&#xff0c;非常方便&#xff0c;快速。我刚好在工作场…

JavaScript--作用域是什么

作用域是什么 编译原理 在传统的编译语言中&#xff0c;程序中的一段源代码在执行之前会经历三个步骤。成为编译 分词/词法分析 这个过程由字符组成的字符串分解成有意义的代码块&#xff0c;这些代码块成为词法单元。 分词和词法分析之间的主要差异在于词法单元的识别是有…

知识点总结

1、Uboot的流程调用&#xff1a; 1.1、cmd_process函数是怎么被调用到的&#xff1a; cmd_process在common/command.c 1.2、uboot阶段断电&#xff0c;后续起不来&#xff0c;可能要换线去使用&#xff0c;也许和电源线有关 2、git 相关使用 2.1 .gitignore相关的使用 1、…

北京证券公司港股通交易佣金手续费最低是多少?万0.8?港股通纳入规则是怎么样的?

港股通交易佣金概述 港股通的交易佣金可能会因证券公司和投资者的不同而有所差异。 北京证券公司的港股通交易佣金最低可能万分之零点八&#xff08;0.008%&#xff09;&#xff0c;但这需要投资者与证券公司客户经理了解&#xff0c;进行沟通和申请。 一般来说&#xff0c;…

我手握多篇顶会一作,引用量几百,却连个像样的博士offer都申请不到

卷&#xff0c;卷&#xff0c;卷。在当下整个 AI 领域&#xff0c;一切价值衡量标准仿佛都在经历一场恶性的通货膨胀…… 让我们想象这样一个角色&#xff1a; 一个来自普通家庭的学生刻苦努力的完成了自己的学业。他在领域内的顶级会议中发表了多篇论文&#xff0c;并且其论文…

OpenHarmony应用开启Service以及完成自启动和常驻

一.背景 由于有需求实现一个后台常驻服务,这里就是来实现在鸿蒙里面如何实现后台服务并且实现自启动和常驻 二.添加服务 如下来添加服务 然后此时直接运行这个hap是报错的,如下: 此处参考: 应用中添加ServiceExtensionAbility然后安装HAP时提示“code:9568344 error: inst…

【NumPy】全面解析NumPy的bitwise_xor函数:高效按位异或操作指南

&#x1f9d1; 博主简介&#xff1a;阿里巴巴嵌入式技术专家&#xff0c;深耕嵌入式人工智能领域&#xff0c;具备多年的嵌入式硬件产品研发管理经验。 &#x1f4d2; 博客介绍&#xff1a;分享嵌入式开发领域的相关知识、经验、思考和感悟&#xff0c;欢迎关注。提供嵌入式方向…

I.MX6ULL主频和时钟配置实验

系列文章目录 I.MX6ULL主频和时钟配置实验 I.MX6ULL主频和时钟配置实验 系列文章目录一、前言二、I.MX6U 时钟系统详解三、硬件原理四、 7 路 PLL 时钟源五、时钟树简介六、内核时钟设置七、PFD 时钟设置八、AHB、IPG 和 PERCLK 根时钟设置九、实验程序编写十、编译下载10.1编写…

探索Python中的随机数生成与统计分析

新书上架~&#x1f447;全国包邮奥~ python实用小工具开发教程http://pythontoolsteach.com/3 欢迎关注我&#x1f446;&#xff0c;收藏下次不迷路┗|&#xff40;O′|┛ 嗷~~ 目录 一、随机数的魅力与实用性 1. 随机数生成基础 2. 批量生成随机数 二、随机数的高级应用&a…

Linux——Linux服务管理

服务管理大作业要求&#xff1a; 基本拓扑如下&#xff1a; 按照要求完成基本的系统管理任务&#xff1a; 完成所有系统的主机名、网络配置&#xff1b; 本次作业共需要3台虚拟机&#xff0c;分别作为客户端、综合应用服务器、存储服务器。三台虚拟机操作系统均为CentOS-Stream…