深入理解流(Streams)—— 声明式数据处理的艺术

1. 引言

大家好!欢迎来到本系列博客的第三篇。在前两篇文章中,我们已经领略了 Java 8 中 行为参数化Lambda 表达式 的魅力。

  • 在第 1 章 Java行为参数化:从啰嗦到简洁的代码进化中,我们了解到如何通过将行为(代码块)作为参数传递给方法,使代码更灵活、可复用。
  • 在第 2 章 Java 8 Lambda表达式详解:从入门到实践中,我们深入学习了 Lambda 表达式,它是实现行为参数化的简洁而强大的工具。

强烈建议先阅读前两篇文章,它们为理解今天的主题——Java 8 中的“流”(Streams)——奠定了基础。

那么,什么是“流”?它为何如此重要?

简而言之,Java 8 的“流”提供了一种全新的、声明式的处理数据的方式。它允许你以类似于 SQL 查询的风格操作集合(及其他数据源),无需编写冗长的循环和条件语句。

想象一下工厂的流水线:原材料(数据)从一端进入,经过一系列处理工序(操作),最终产出成品。Java 8 中,“流”就像这条流水线,数据在其中流动,我们可以通过各种“流操作”对其进行 筛选转换排序分组 等。

本篇我们将深入探讨“流”的方方面面:

  • 流的定义
  • 流的特性
  • 流与集合的区别
  • 流的核心操作
  • 如何利用流编写更简洁、高效、易于理解的代码

让我们一起开启 Java 8“流”的探索之旅!

2. 流是什么?(What are Streams?)

引言中,我们用流水线类比了“流”。现在,让我们揭开“流”的神秘面纱。

流是“从支持数据处理操作的源生成的一系列元素”

——《Java 8 in Action》

让我们拆解这个定义:

  • 一系列元素: 与集合类似,流也是一系列元素的集合。你可以把一堆苹果放进篮子(集合),也可以把它们放在流水线(流)上。关键在于,流关注的是如何处理这些元素,而不是如何存储它们。

  • 源: 流中的元素从哪里来?答案是“源”。它可以是:

    • 集合 (List, Set 等)
    • 数组
    • I/O 资源 (文件等)
    • 生成函数 (例如,产生无限序列的函数) 流本身不存储数据,它只是从源头获取数据。
  • 数据处理操作: 这是流的核心!流提供了一套丰富的操作,让你对数据进行各种处理,类似数据库查询操作:

    • filter: 筛选符合条件的元素。
    • map: 将元素转换为另一种形式(如小写字母转大写)。
    • reduce: 将所有元素组合成一个结果(如求和)。
    • sort: 排序。
    • … 还有很多!
  • 内部迭代: 通常,我们用 for 循环或 forEach 显式遍历集合(外部迭代)。而流则不同,它在内部迭代。你只需要告诉流_你想要做什么_,无需关心_如何做_。这使代码更简洁,也更容易优化(如并行处理)。

流不是新的数据结构,而是更高层次的抽象。它专注于 做什么(数据处理),而不是 怎么做(迭代细节)。流像管道,数据从源头流入,经过一系列处理,产生结果。这种声明式编程风格使代码更易读、维护。

3. 流与集合(Streams vs. Collections)

Java 8 的「流」常与集合(Collections)比较。虽都用于处理数据,但两者差异显著。理解这些差异对于有效使用流至关重要。

相同点:

  • 存储元素: 流和集合都可存储一系列元素。

不同点:

特性集合 (Collections)流 (Streams)
主要目的存储和访问元素对元素进行计算
何时计算元素在加入集合时就已计算好元素在需要时才计算(延迟计算/惰性求值
迭代方式外部迭代(用户代码控制迭代)内部迭代(流库自身控制迭代)
遍历次数可以多次遍历只能遍历一次
数据修改可以添加、删除、修改集合中的元素流操作通常不修改数据源
数据结构是一种数据结构,主要目的是以特定的时间/空间复杂度存储和访问数据不是数据结构,它没有存储空间,主要目的是对数据源进行计算。

详细解释几个关键区别:

3.1 只能遍历一次

这是流的重要限制。一旦对流执行终端操作(如 forEachcollect),流就被“消费”,不能再用。再次遍历会抛 IllegalStateException

代码示例:

List<String> names = Arrays.asList("Alice", "Bob", "Charlie");
Stream<String> nameStream = names.stream();// 第一次遍历:打印名字
nameStream.forEach(System.out::println);// 第二次遍历:会抛出异常!
// nameStream.forEach(System.out::println); // java.lang.IllegalStateException: stream has already been operated upon or closed

这与集合形成对比,集合可多次遍历。

3.2 外部迭代与内部迭代
  • 外部迭代(集合): 编写显式循环(如 for-each)遍历集合,并处理元素。你完全掌控迭代过程。
  • 内部迭代(流): 只需告诉流你想做什么(如筛选长度大于3的名字),流内部进行迭代和处理。无需编写循环,代码更简洁。

代码示例:

List<String> names = Arrays.asList("Alice", "Bob", "Charlie", "David");// 外部迭代(集合)
List<String> longNames1 = new ArrayList<>();
for (String name : names) {if (name.length() > 3) {longNames1.add(name);}
}
System.out.println(longNames1); // [Alice, Charlie, David]// 内部迭代(流)
List<String> longNames2 = names.stream().filter(name -> name.length() > 3).collect(Collectors.toList());
System.out.println(longNames2); // [Alice, Charlie, David]

流(内部迭代)代码更简洁、易读,更接近声明式编程。我们描述了想要什么(筛选长度大于3的名字),未指定如何做(循环和条件判断)。

3.3 延迟计算/惰性求值

这是流的重要特性。流的中间操作(如filter,map)延迟计算。遇到终端操作前,中间操作不执行。终端操作触发时,才计算。

4. 流操作详解 (Stream Operations in Detail)

流的强大在于其丰富的操作,让你以声明式方式处理数据。操作分两类:中间操作终端操作。理解这两类操作及如何协同工作,是掌握流的关键。

4.1 中间操作 (Intermediate Operations)

特点:

  • 返回另一个流: 每个中间操作返回新流。可将多个中间操作链接,形成“流水线”。
  • 延迟执行(Lazy): 中间操作不立即执行,只构建流水线。终端操作触发时,中间操作才执行。

常见中间操作:

操作描述示例
filter筛选符合条件的元素stream.filter(x -> x > 5)
map将每个元素映射为另一个元素(类型可能不同)stream.map(String::toUpperCase)
limit截取流的前 N 个元素stream.limit(10)
skip跳过流的前 N 个元素stream.skip(5)
distinct去除流中的重复元素(根据 equalsstream.distinct()
sorted对流中的元素排序(自然排序或根据 Comparatorstream.sorted() stream.sorted(Comparator.reverseOrder())
peek对流中每个元素执行一个操作,但不改变流内容(主要用于调试)stream.peek(System.out::println)
flatMap将每个元素转换为一个流,然后将这些流合并为一个流。stream.flatMap(Collection::stream)

代码示例 (中间操作链):

List<String> names = Arrays.asList("Alice", "Bob", "Charlie", "David", "Eve");List<String> result = names.stream().filter(name -> name.length() > 3)  // 筛选长度大于3的名字.map(String::toLowerCase)          // 转小写.sorted()                          // 排序.collect(Collectors.toList());     // 收集结果System.out.println(result); // [alice, charlie, david]

filtermapsorted 是中间操作。它们链接成流水线。注意,直到 collect(终端操作)被调用,中间操作才执行。

4.2 终端操作 (Terminal Operations)

特点:

  • 产生结果或副作用: 终端操作触发流水线执行,产生结果(非流值)或副作用(如打印)。
  • 消费流: 终端操作执行后,流被消费,不能再用。

常见终端操作:

操作描述示例
forEach对流中每个元素执行一个操作(副作用)stream.forEach(System.out::println)
count返回流中元素个数long count = stream.count()
collect将流中元素收集到集合(或其他数据结构)List<String> list = stream.collect(Collectors.toList())
reduce将流中元素组合成一个值(如求和、求最大值)Optional<Integer> sum = stream.reduce(Integer::sum)
anyMatch检查是否至少有一个元素匹配给定条件boolean hasLongName = stream.anyMatch(s -> s.length() > 5)
allMatch检查是否所有元素都匹配给定条件boolean allUpperCase = stream.allMatch(s -> Character.isUpperCase(s.charAt(0)))
noneMatch检查是否没有元素匹配给定条件boolean noEmptyString = stream.noneMatch(String::isEmpty)
findFirst返回流中第一个元素(Optional)Optional<String> first = stream.findFirst()
findAny返回流中任意一个元素(Optional,并行流中更常用)Optional<String> any = stream.findAny()

代码示例 (终端操作):

List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5);// 求和
int sum = numbers.stream().reduce(0, Integer::sum); // 初始值为0,用 Integer.sum() 累加
System.out.println("Sum: " + sum); // Sum: 15// 查找第一个偶数
Optional<Integer> firstEven = numbers.stream().filter(n -> n % 2 == 0).findFirst();
firstEven.ifPresent(System.out::println); // 2 (若存在偶数)// 检查是否所有数字都大于0
boolean allPositive = numbers.stream().allMatch(n -> n > 0);
System.out.println("All positive: " + allPositive); // All positive: true

5. 流的“按需计算”(On-Demand Computation)

前面多次提到流的“延迟计算”/“惰性求值”。现在深入探讨。

5.1 什么是“按需计算”?

流中元素只在真正需要时才计算。与集合对比,集合中所有元素在创建时就已存在于内存。

5.2 为什么“按需计算”重要?

带来几个关键优势:

  1. 效率提升: 若非所有元素都需处理,“按需计算”可避免不必要计算,提高效率。处理大数据集时,优势明显。

  2. 短路操作: “按需计算”使“短路操作”(如 findFirstanyMatch)成为可能。找到满足条件的元素,就无需处理剩余元素。

  3. 无限流: “按需计算”使创建“无限流”(Infinite Streams)成为可能。无限流无固定结尾,可根据需要生成无限多元素。

5.3 “按需计算”如何工作?

通过中间操作和终端操作协同实现。

  • 中间操作:懒惰”。只构建处理流水线,不立即执行。
  • 终端操作:急切”。终端操作被调用,触发流水线执行。

终端操作需要元素时,流水线上中间操作才处理数据源。中间操作通常非一次处理一个元素,而是按需逐个处理。

代码示例(演示“按需计算”):

 List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9, 10);Optional<Integer> firstEvenGreaterThan5 = numbers.stream().filter(n -> {System.out.println("Filtering: " + n); // 打印过滤操作的中间结果return n % 2 == 0;}).filter(n -> {System.out.println("Filtering again: "+n);return n > 5;}).findFirst();firstEvenGreaterThan5.ifPresent(n -> System.out.println("Result: " + n));

输出:

Filtering: 1
Filtering: 2
Filtering again: 2
Filtering: 3
Filtering: 4
Filtering again: 4
Filtering: 5
Filtering: 6
Filtering again: 6
Result: 6

分析:

从输出可见:

  1. 并非所有数字都被 filter 处理。
  2. findFirst 找到第一个满足条件的元素(6),后续元素不再处理。
  3. 两个filter非独立,而是交替执行。

这就是“按需计算”。流只处理必要元素,找到 findFirst 要求的结果。

6.总结

Java 8 的流(Streams)是一种强大而优雅的数据处理工具。它通过声明式、函数式的风格,使代码更简洁、易读、高效。

在这篇文章中,我们深入探讨了:

  • 流的本质: 一种支持数据处理操作的元素序列,强调“做什么”而非“怎么做”。
  • 流与集合的区别: 延迟计算、内部迭代、一次性遍历等。
  • 流的操作: 中间操作(构建流水线)和终端操作(触发计算)。
  • 按需计算: 流的关键特性,提高效率、支持短路操作和无限流。

掌握了流,你就掌握了 Java 8 中最强大的武器之一。在后续的文章中,我们会进一步探索流的高级用法,包括并行流、自定义收集器等。敬请期待!

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

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

相关文章

【蓝桥杯嵌入式】6_定时器输入捕获

全部代码网盘自取 链接&#xff1a;https://pan.baidu.com/s/1PX2NCQxnADxYBQx5CsOgPA?pwd3ii2 提取码&#xff1a;3ii2 这是两个信号发生器&#xff0c;可以通过调节板上的两个电位器R39和R40调节输出频率。 将PB4、PA15选择ch1&#xff0c;两个信号发生器只能选择TIM3和TIM…

详解SQLAlchemy的函数relationship

在 SQLAlchemy 中&#xff0c;relationship 是一个非常重要的函数&#xff0c;用于定义模型之间的关系。它用于在 ORM 层面上表示数据库表之间的关联关系&#xff08;如 1 对 1、1 对多和多对多&#xff09;。relationship 的主要作用是提供一个高级接口&#xff0c;用于在模型…

iOS 音频录制、播放与格式转换

iOS 音频录制、播放与格式转换:基于 AVFoundation 和 FFmpegKit 的实现 在 iOS 开发中,音频处理是一个非常常见的需求,比如录音、播放音频、音频格式转换等。本文将详细解读一段基于 AVFoundation 和 FFmpegKit 的代码,展示如何实现音频录制、播放以及 PCM 和 AAC 格式之间…

数据结构与算法(test1)

一、树和二叉树 1. 看图&#xff0c;完成以下填空 (1).树的度为________。 (2).树中结点的最大层次&#xff0c;称为树的_____或树的______&#xff0c;值是______。 (3).结点A和B的度分别为________ 和 ________。 (4).结点A是结点B的________。 (5).结点B是结点A的________…

新版AndroidStudio 修改 jdk版本

一、问题 之前&#xff0c;在安卓项目中配置JDK和Gradle的过程非常直观&#xff0c;只需要进入Android Studio的File菜单中的Project Structure即可进行设置&#xff0c;十分方便。 如下图可以在这修改JDK: 但是升级AndroidStudio之后&#xff0c;比如我升级到了Android Stu…

cursor 开发java项目教程简单上手

1.官网下载 Cursor - The AI Code Editor 下载完后注册账号&#xff0c;可以使用无限邮的方式 注册完之后 设置中文 可以选择设置为中文 Ctrl Shift X 进入设置页面输入chinese 然后重启 更改jdk跟maven仓库设置 ctrlshiftp 打开输入框后输入json&#xff0c;把下面代码…

安装和使用 Ollama(实验环境windows)

下载安装 下载 https://ollama.com/download/windows 安装 Windows 安装 如果直接双击 OllamaSetup.exe 安装&#xff0c;默认会安装到 C 盘&#xff0c;如果需要指定安装目录&#xff0c;需要通过命令行指定安装地址&#xff0c;如下&#xff1a; # 切换到安装目录 C:\Use…

LQB(0)-python-基础知识

一、Python开发环境与基础知识 python解释器&#xff1a;用于解释python代码 方式&#xff1a; 1.直接安装python解释器 2.安装Anaconda管理python环境 python开发环境&#xff1a;用于编写python代码 1.vscode 2.pycharm # 3.安装Anaconda后可以使用网页版的jupyter n…

C# 中记录(Record)详解

从C#9.0开始&#xff0c;我们有了一个有趣的语法糖&#xff1a;记录(record)   为什么提供记录&#xff1f; 开发过程中&#xff0c;我们往往会创建一些简单的实体&#xff0c;它们仅仅拥有一些简单的属性&#xff0c;可能还有几个简单的方法&#xff0c;比如DTO等等&#xf…

使用 CSS 实现透明效果

在 CSS 中&#xff0c;实现透明效果有几种方法&#xff0c;具体使用哪种方法取决于具体需求。以下是一些常见的方法&#xff1a; 使用 opacity 属性&#xff1a; opacity 属性可以设置整个元素的透明度&#xff0c;包括其所有的子元素。 .transparent { opacity: 0.5; /* 0 表…

C语言:函数栈帧的创建和销毁

目录 1.什么是函数栈帧2.理解函数栈帧能解决什么问题3.函数栈帧的创建和销毁的过程解析3.1 什么是栈3.2 认识相关寄存器和汇编指令3.3 解析函数栈帧的创建和销毁过程3.3.1 准备环境3.3.2 函数的调用堆栈3.3.3 转到反汇编3.3.4 函数栈帧的创建和销毁 1.什么是函数栈帧 在写C语言…

25/2/6 <机器人基础> 运动学中各连杆的变换矩阵求法

变换矩阵 机器人通常包含多个关节和连杆&#xff0c;每个关节和连杆都有自己的局部坐标系。变换矩阵能够将一个点或向量从一个坐标系转换到另一个坐标系&#xff0c;从而实现对机器人各个部件位置和姿态的统一描述 变换矩阵能够将复杂的运动分解为旋转和平移的组合。通过矩阵乘…

AllData数据中台核心菜单十二:数据同步平台

&#x1f525;&#x1f525; AllData大数据产品是可定义数据中台&#xff0c;以数据平台为底座&#xff0c;以数据中台为桥梁&#xff0c;以机器学习平台为中层框架&#xff0c;以大模型应用为上游产品&#xff0c;提供全链路数字化解决方案。 ✨奥零数据科技官网&#xff1a;…

【FPGA】 MIPS 12条整数指令 【3】

实现乘除 修改框架 EX&#xff1a;实现带符号乘除法和无符号乘除法 HiLo寄存器&#xff1a;用于存放乘法和除法的运算结果。Hi、Lo为32bit寄存器。电路描述与实现RegFile思想一致 仿真 代码 DataMem.v include "define.v"; module DataMem(input wire clk,input…

文件基础IO

理解"文件" 1-1 狭义理解 文件在磁盘里磁盘是永久性存储介质&#xff0c;因此文件在磁盘上的存储是永久性的磁盘是外设&#xff08;即是输出设备也是输入设备&#xff09;磁盘上的文件 本质是对文件的所有操作&#xff0c;都是对外设的输入和输出简称IO 1-2 广义理…

Unity 简易的UI框架

核心内容 UIType.cs namespace MYTOOL.UI {/// <summary>/// UI层级/// </summary>public enum UILayer{/// <summary>/// 主界面层/// </summary>MainUI 0,/// <summary>/// 普通界面层/// </summary>NormalUI 1,/// <summary>/…

VUE2双向绑定的原理

文章目录 VUE2双向绑定的原理1. 什么是双向绑定2. 双向绑定的原理2.1 ViewModel的重要作用2.2 双向绑定的流程 3. 双向绑定的实现3.1 data响应化处理3.2 Compile编译3.3 依赖收集 VUE2双向绑定的原理 1. 什么是双向绑定 讲双向绑定先讲单项绑定&#xff0c;啥叫单项绑定&…

4G核心网的演变与创新:从传统到虚拟化的跨越

4G核心网 随着移动通信技术的不断发展&#xff0c;4G核心网已经经历了从传统的硬件密集型架构到现代化、虚拟化网络架构的重大转型。这一演变不仅提升了网络的灵活性和可扩展性&#xff0c;也为未来的5G、物联网&#xff08;LOT&#xff09;和边缘计算等技术的发展奠定了基础。…

HTML排版标签、语义化标签、块级和行内元素详解

目录 前言 一、HTML中的排版标签 1. 文本相关标签 1.1 标题标签 ~ 1.2 段落标签 1.3 强调和加粗 1.4 换行标签 1.5 水平线标签 二、HTML中的语义化标签 2.1 语义化标签概述 2.2 常见的语义化标签 示例&#xff08;核心代码部分&#xff09;&#xff1a; 三、HTM…

【字节青训营-7】:初探 Kitex 字节微服务框架(使用ETCD进行服务注册与发现)

本文目录 一、Kitex概述二、第一个Kitex应用三、IDL四、服务注册与发现 一、Kitex概述 长话短说&#xff0c;就是字节跳动内部的 Golang 微服务 RPC 框架&#xff0c;具有高性能、强可扩展的特点&#xff0c;在字节内部已广泛使用。 如果对微服务性能有要求&#xff0c;又希望…