面试官:用Stream优化这段代码。你:for循环?对不起,下一题!一文入门,实现面试降维打击!

在 Java 8 引入的众多特性中,Stream(流)无疑是最具变革性的之一。它让我们能以声明式、函数式的方式处理集合数据——无需显式循环,就能完成过滤、映射、聚合等操作。

我们可以将 Java 流看作是一个数据流经的管道。无需手动编写循环和条件语句来处理列表,只需告诉 Java 对每个元素应执行什么操作,Java 流 API 会负责处理内部的实现方式。

Java 流并不存储数据。相反,它对诸如 ListSetMap 或数组等现有的数据源进行操作。流会对数据源应用一系列操作。

我们将向你介绍 Java 流,学习如何从 Java 集合创建流,首次了解流管道,并了解 Lambda 表达式、方法引用和其他函数式编程元素如何与 Java 流协同工作。还将学习如何将收集器和可选链与 Java 流结合使用,以及在程序中何时应该使用或不应该使用流。

总之,本文将带你轻松入门 Java 流,从创建到组合,用简洁优雅的代码释放数据处理的真正潜力。

一、流 VS 集合

许多开发者会被 Java 流和 Java 集合之间的区别所困扰:

  • 集合(如 ArrayListHashSet)用于存储。它们将数据保存在内存中以供你访问。
  • 流关注的是行为。它们描述对数据要做什么,而不是如何存储数据。

打个比方,可以把集合想象成存放食材的橱柜,而流则是将这些食材做成一顿饭的食谱。

流通过描述要做什么而不是如何去做,赋予 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]

乍一看,这个流管道似乎应该:

  1. 过滤掉 "guy""bob"(因为它们的长度不大于 3)。
  2. 将其余的转换为大写。
  3. 对它们进行排序。

但实际上,这个管道什么都没有做。

原因是 Java 中的流是惰性的

  • 所有这些调用(filtermapsorted)都是中间操作。
  • 它们不会立即执行。相反,它们只记录操作计划。
  • 只有当你添加一个终端操作,如.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 高效地去完成。

StreamCollectorOptional 结合使用,能让现代 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%免费

在这里插入图片描述

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值