
在 Java 8 引入的众多特性中,Stream(流)无疑是最具变革性的之一。它让我们能以声明式、函数式的方式处理集合数据——无需显式循环,就能完成过滤、映射、聚合等操作。
我们可以将 Java 流看作是一个数据流经的管道。无需手动编写循环和条件语句来处理列表,只需告诉 Java 对每个元素应执行什么操作,Java 流 API 会负责处理内部的实现方式。
Java 流并不存储数据。相反,它对诸如 List、Set、Map 或数组等现有的数据源进行操作。流会对数据源应用一系列操作。
我们将向你介绍 Java 流,学习如何从 Java 集合创建流,首次了解流管道,并了解 Lambda 表达式、方法引用和其他函数式编程元素如何与 Java 流协同工作。还将学习如何将收集器和可选链与 Java 流结合使用,以及在程序中何时应该使用或不应该使用流。
总之,本文将带你轻松入门 Java 流,从创建到组合,用简洁优雅的代码释放数据处理的真正潜力。
一、流 VS 集合
许多开发者会被 Java 流和 Java 集合之间的区别所困扰:
- 集合(如
ArrayList或HashSet)用于存储。它们将数据保存在内存中以供你访问。 - 流关注的是行为。它们描述对数据要做什么,而不是如何存储数据。
打个比方,可以把集合想象成存放食材的橱柜,而流则是将这些食材做成一顿饭的食谱。
流通过描述要做什么而不是如何去做,赋予 Java 一种函数式和声明式的特性。
二、为什么使用流
Java 开发者会出于多种原因偏向并使用流:
- 代码更简洁,可替代嵌套循环和条件语句。
- 样板代码更少,无需再编写手动的
for循环。 - 逻辑更具可读性,流管道读起来就像自然语言。
通过比较循环和流,我们就能初步看出这些差异。
在 Java 中,流常常会取代传统的循环,一旦你开始使用流,就很难再回头使用传统方式了。下面是一个典型的 for 循环示例:
List<String> names = List.of("patrick", "mike", "james", "bill");List<String> result = new ArrayList<>();for (String name : names) { if (name.length() > 4) { result.add(name.toUpperCase()); }}Collections.sort(result);System.out.println(result);
如果使用流呢?
List<String> names = List.of("patrick", "mike", "james", "bill");List<String> result = names.stream() .filter(name -> name.length() > 4) .map(String::toUpperCase) .sorted() .toList();System.out.println(result);
与循环不同,流的操作语句读起来几乎就像英语:“取出名字,按长度过滤,转换为大写,进行排序,然后收集到一个列表中”。 操作完成后,输出将是:[james, patrick]。
三、从集合创建流

流可以从多种来源开始。把下面所有的示例都看作是 “打开水龙头” 的方式。
以下是如何从集合(在这个例子中是一个包含名字的列表)创建流的方法:
List<String> names = List.of("James", "Bill", "Patrick");Stream<String> nameStream = names.stream();
以下是如何从 Map 创建流的方法:
Map<Integer, String> idToName = Map.of(1, "James", 2, "Bill");Stream<Map.Entry<Integer, String>> entryStream = idToName.entrySet().stream();
从数组创建流:
String[] names = {"James", "Bill", "Patrick"};Stream<String> nameStream = Arrays.stream(names);
当然,也可以使用 Stream.of() 创建流:
Stream<Integer> numberStream = Stream.of(1, 2, 3, 4, 5);
使用 Stream.of(),你可以传入任何类型的值或对象来创建一个流。
当你手头没有集合或数组时,这是一种快速创建流的简便方法。对于小型且固定的数据集合或快速测试来说十分适用。
四、Stream.generate() 创建流
Stream.generate() 方法创建一个无限流。只要管道需要,它就会持续生成值:
Stream.generate(() -> "hello").forEach(System.out::println);
这个流永远不会停止。使用 limit() 方法来控制它:
Stream.generate(Math::random) .limit(5) .forEach(System.out::println);
Stream.generate() 和 Stream.iterate() 都可以生成无限序列。一定要使用限制操作或短路操作,避免无限执行。
如果你需要安全地返回一个空流而不是 null,可以使用 Stream.empty():
Stream<String> emptyStream = Stream.empty();
这样做避免了空指针检查,并使返回流的方法更安全、更简洁。
五、流操作

流有中间(延迟)操作和终端(执行)操作。这两种类型的操作共同构成了数据管道。
1.中间操作(途中转换)
中间流操作不会立即触发执行。它们只是在过程中添加处理步骤:
map:转换每个元素。filter:仅保留符合条件的元素。sorted:对元素进行排序。distinct:去除重复项。limit/skip:裁剪流。flatMap:将嵌套结构(例如列表的列表)扁平化为一个流。peek:让你在元素流过时查看它们(非常适合调试以及记录日志)。takeWhile:持续提取元素,直到条件为假(类似于有条件的限制)。dropWhile:在条件为真时跳过元素,然后保留其余元素。
2.流是惰性的
流首先会准备好所有步骤(过滤、映射、排序),但在终端操作触发处理之前,不会有任何实际操作发生。这种惰性求值机制通过仅处理所需的数据,提高了流操作的效率。
以这个流管道为例:
List<String> names = List.of("james", "bill", "patrick", "guy", "bob");names.stream() .filter(n -> n.length() > 3) // 保留长度超过3个字符的名字 .map(String::toUpperCase) // 转换为大写 .sorted(); // 按照字母顺序排序System.out.println("List result: " + names);
结果是: [james, bill, patrick, guy, bob]。
乍一看,这个流管道似乎应该:
- 过滤掉
"guy"和"bob"(因为它们的长度不大于3)。 - 将其余的转换为大写。
- 对它们进行排序。
但实际上,这个管道什么都没有做。
原因是 Java 中的流是惰性的:
- 所有这些调用(
filter、map、sorted)都是中间操作。 - 它们不会立即执行。相反,它们只记录操作计划。
- 只有当你添加一个终端操作,如
.toList()、forEach()或count()时,这个计划才会运行。
由于上述代码中没有终端操作,该流管道被丢弃,原始列表将保持不变地打印出来。
3.终端操作(上菜)
现在我们可以来看看流的第二类操作。终端操作会触发流运行并产生一个结果:
forEach():对每个元素执行某些操作。collect():将元素收集到一个集合中。toList():将所有元素收集到一个不可变的列表中(Java 16 及以上版本)。reduce():将多个元素合并为单个结果(如求和、求积等)。count():元素有多少个?findFirst():返回第一个符合过滤条件的元素(在顺序很重要的情况下很有用)。findAny():返回任何一个匹配的元素(在不保证顺序的并行流中特别有用)。toArray():将结果收集到一个数组中。min(Comparator) / max(Comparator):根据一个比较器找到最小或最大的元素。anyMatch(predicate):是否有任何元素匹配?allMatch(predicate):所有元素都匹配吗?noneMatch(predicate):没有元素匹配吗?
下面是一个使用终端操作的流的示例:
List<String> names = List.of("james", "bill", "patrick", "guy");List<String> result = names.stream() .filter(n -> n.length() > 3) .map(String::toUpperCase) .sorted() .toList(); // 终端操作方法在这里触发执行System.out.println(result);
上述代码输出将是:[BILL, JAMES, PATRICK]。
4.流是一次性的
一旦流被处理,它就会被消耗掉且不能再被重复使用。终端操作会关闭流:
List<String> names = List.of("James", "Bill", "Patrick");Stream<String> s = names.stream();s.forEach(System.out::println); // OKs.count(); // IllegalStateException —— 已被处理
在这段代码中,第一次调用会使所有数据通过流水线,之后流就会被关闭。如果需要,请创建一个新的流:
long count = names.stream().count(); // 正确做法:新的流实例
5.流管道
我们给出一个包含中间操作和终态操作的流管道示例:
List<String> result = names.stream() // 数据源 .filter(n -> n.length() > 3) // 中间操作 .map(String::toUpperCase) // 中间操作 .sorted() // 中间操作 .toList(); // 终端操作
六、与集合共舞

除了流,Java 8 还引入了收集器,可用于描述如何收集处理后的数据。
收集到列表会创建一个新的、不可修改的、包含长度超过三个字符的名称的列表。不可变的结果使流代码更安全且更具函数式编程风格:
List<String> list = names.stream () .filter (n -> n.length () > 3) .toList (); // Java 16 及以上版本可用
在这里,我们将结果收集到一个集合中,这样会自动去除重复项。当唯一性比顺序更重要时,就使用集合:
Set<String> set = names.stream() .map(String::toUpperCase) .collect(Collectors.toSet());
在这里,我们将数据收集到一个 Map 中,其中键是字符串的长度,值是 name 本身:
Map<Integer, String> map = names.stream() .collect(Collectors.toMap( String::length, n -> n ));
如果多个名称的长度相同,就会发生冲突。可以使用合并函数来处理这种情况:
Map<Integer, String> safeMap = names.stream() .collect(Collectors.toMap( String::length, n -> n, (a, b) -> a // 如果键发生冲突,保留第一个值。 ));
1.连接字符串
Collectors.joining() 可以使用你选择的任何分隔符将流中的所有元素合并为一个字符串。你可以使用 |、;,甚至 \n:
List<String> names = List.of("Bill", "James", "Patrick");String result = names.stream() .map(String::toUpperCase) .collect(Collectors.joining(", "));System.out.println(result);
输出为:BILL, JAMES, PATRICK。
2.数据分组
Collectors.groupingBy() 按键(这里是字符串长度)对元素进行分组,并返回一个 Map<Key,List<Value>>:
List<String> names = List.of("james", "linus", "john", "bill", "patrick");Map<Integer, List<String>> grouped = names.stream() .collect(Collectors.groupingBy(String::length));
输出为:{4=[john, bill], 5=[james, linus], 7=[patrick]}。
3.数字汇总
你也可以使用集合进行汇总:
List<Integer> numbers = List.of(3, 5, 7, 2, 10);IntSummaryStatistics stats = numbers.stream() .collect(Collectors.summarizingInt(n -> n));System.out.println(stats);
输出为:IntSummaryStatistics {count=5, sum=27, min=2, average=5.4, max=10}。
如果你只想要平均值,可以这样做:
double avg = numbers.stream() .collect(Collectors.averagingDouble(n -> n));
七、函数式编程

前面提到过,流融合了函数式和声明式的元素。下面我们来看看流中的一些函数式编程元素。
1.Lambda 表达式与方法引用
Lambda 表达式可以在内部定义行为,而方法引用则可以重用现有的方法:
names.stream() .filter(name -> name.length() > 3) .map(String::toUpperCase) .forEach(System.out::println);
2.map() vs flatMap()
根据经验来说:
- 当你有一个输入并且想要一个输出时,使用
map()方法。 - 当你有一个输入并且想要多个输出(展平后的)时,使用
flatMap()方法。
以下是一个在流中使用 map() 方法的示例:
List<List<String>> nested = List.of( List.of("james", "bill"), List.of("patrick"));nested.stream() .map(list -> list.stream()) .forEach(System.out::println);
输出会是:
java.util.stream.ReferencePipeline$Head@5ca881b5java.util.stream.ReferencePipeline$Head@24d46ca6
有两行是因为有两个内部列表,所以你需要两个 Stream 对象。另外要注意哈希值会有所不同。
如果使用 flatMap() 呢:
nested.stream() .flatMap(List::stream) .forEach(System.out::println);
输出如下:
james bill patrick
对于更深层次的嵌套:
List<List<List<String>>> deep = List.of( List.of(List.of("James", "Bill")), List.of(List.of("Patrick")));List<String> flattened = deep.stream() .flatMap(List::stream) .flatMap(List::stream) .toList();System.out.println(flattened);
输出为:[James, Bill, Patrick]。
八、Optional 操作符
Optional 操作符是另一种可与流结合使用的有用操作:
List<String> names = List.of("James", "Bill", "Patrick");String found = names.stream() .filter(n -> n.length() > 6) .findFirst() .map(String::toUpperCase) .orElse("NOT FOUND");System.out.println(found);
输出会是:NOT FOUND。
findFirst() 会返回一个 Optional 对象,它可以安全地表示一个可能不存在的值。如果没有匹配项,.orElse() 会提供一个备用值。出于同样的原因,诸如 findAny()、min() 和 max() 等方法也会返回 Optional 对象。
九、总结
Java 的 Stream API 彻底改变了我们处理数据的方式。你只需声明“要做什么”——比如过滤、映射或排序——具体的实现细节就交给 Java 高效地去完成。
将 Stream、Collector 和 Optional 结合使用,能让现代 Java 代码变得更简洁、清晰,也更健壮。当你需要对集合进行转换或分析时,Stream 是理想的选择;但如果是涉及索引操作或频繁变动的任务,传统方式可能更合适。
一旦你真正掌握了 Stream 的用法,恐怕就很难再回到手写循环的老路了。在熟悉本文介绍的基础知识后,不妨进一步探索并行流、基本类型专用流(如 IntStream)以及自定义 Collector 等高级特性。最重要的是多动手实践——试着运行和修改文中的示例代码,只有通过实际操作,才能真正内化这些技能。
如何高效转型Al大模型领域?
作为一名在一线互联网行业奋斗多年的老兵,我深知持续学习和进步的重要性,尤其是在复杂且深入的Al大模型开发领域。为什么精准学习如此关键?
- 系统的技术路线图:帮助你从入门到精通,明确所需掌握的知识点。
- 高效有序的学习路径:避免无效学习,节省时间,提升效率。
- 完整的知识体系:建立系统的知识框架,为职业发展打下坚实基础。
AI大模型从业者的核心竞争力
- 持续学习能力:Al技术日新月异,保持学习是关键。
- 跨领域思维:Al大模型需要结合业务场景,具备跨领域思考能力的从业者更受欢迎。
- 解决问题的能力:AI大模型的应用需要解决实际问题,你的编程经验将大放异彩。
以前总有人问我说:老师能不能帮我预测预测将来的风口在哪里?
现在没什么可说了,一定是Al;我们国家已经提出来:算力即国力!
未来已来,大模型在未来必然走向人类的生活中,无论你是前端,后端还是数据分析,都可以在这个领域上来,我还是那句话,在大语言AI模型时代,只要你有想法,你就有结果!只要你愿意去学习,你就能卷动的过别人!
现在,你需要的只是一份清晰的转型计划和一群志同道合的伙伴。作为一名热心肠的互联网老兵,我决定把宝贵的AI知识分享给大家。 至于能学习到多少就看你的学习毅力和能力了 。

第一阶段(10天):初阶应用
该阶段让大家对大模型 AI有一个最前沿的认识,对大模型 AI 的理解超过 95% 的人,可以在相关讨论时发表高级、不跟风、又接地气的见解,别人只会和 AI 聊天,而你能调教 AI,并能用代码将大模型和业务衔接。
- 大模型 AI 能干什么?
- 大模型是怎样获得「智能」的?
- 用好 AI 的核心心法
- 大模型应用业务架构
- 大模型应用技术架构
- 代码示例:向 GPT-3.5 灌入新知识
- 提示工程的意义和核心思想
- Prompt 典型构成
- 指令调优方法论
- 思维链和思维树
- Prompt 攻击和防范
- …
第二阶段(30天):高阶应用
该阶段我们正式进入大模型 AI 进阶实战学习,学会构造私有知识库,扩展 AI 的能力。快速开发一个完整的基于 agent 对话机器人。掌握功能最强的大模型开发框架,抓住最新的技术进展,适合 Python 和 JavaScript 程序员。
- 为什么要做 RAG
- 搭建一个简单的 ChatPDF
- 检索的基础概念
- 什么是向量表示(Embeddings)
- 向量数据库与向量检索
- 基于向量检索的 RAG
- 搭建 RAG 系统的扩展知识
- 混合检索与 RAG-Fusion 简介
- 向量模型本地部署
- …
第三阶段(30天):模型训练
恭喜你,如果学到这里,你基本可以找到一份大模型 AI相关的工作,自己也能训练 GPT 了!通过微调,训练自己的垂直大模型,能独立训练开源多模态大模型,掌握更多技术方案。
到此为止,大概2个月的时间。你已经成为了一名“AI小子”。那么你还想往下探索吗?
- 为什么要做 RAG
- 什么是模型
- 什么是模型训练
- 求解器 & 损失函数简介
- 小实验2:手写一个简单的神经网络并训练它
- 什么是训练/预训练/微调/轻量化微调
- Transformer结构简介
- 轻量化微调
- 实验数据集的构建
- …
第四阶段(20天):商业闭环
对全球大模型从性能、吞吐量、成本等方面有一定的认知,可以在云端和本地等多种环境下部署大模型,找到适合自己的项目/创业方向,做一名被 AI 武装的产品经理。
- 硬件选型
- 带你了解全球大模型
- 使用国产大模型服务
- 搭建 OpenAI 代理
- 热身:基于阿里云 PAI 部署 Stable Diffusion
- 在本地计算机运行大模型
- 大模型的私有化部署
- 基于 vLLM 部署大模型
- 案例:如何优雅地在阿里云私有部署开源大模型
- 部署一套开源 LLM 项目
- 内容安全
- 互联网信息服务算法备案
- …
学习是一个过程,只要学习就会有挑战。天道酬勤,你越努力,就会成为越优秀的自己。
如果你能在15天内完成所有的任务,那你堪称天才。然而,如果你能完成 60-70% 的内容,你就已经开始具备成为一名大模型 AI 的正确特征了。
这份完整版的大模型 AI 学习资料已经上传优快云,朋友们如果需要可以微信扫描下方优快云官方认证二维码免费领取【保证100%免费】

4328

被折叠的 条评论
为什么被折叠?



