当前位置: 首页 > news >正文

Java Stream进阶:map是“一对一”,flatMap是“一对多”

图片

1. Java 中的 map() 是什么?

让我们从基础开始。
map() 是 Java Stream API 中的一个方法。它能将流 (stream) 中的每个元素转换(或“映射”)为另一种形式。

  • • 简单示例:
    import java.util.List;
    import java.util.stream.Collectors;List<String> names = List.of("Alice", "Bob", "Charlie");// 将字符串列表映射为其长度列表
    List<Integer> nameLengths = names.stream().map(String::length) // 对每个字符串应用 String::length 方法.collect(Collectors.toList());System.out.println(nameLengths); // 输出: [5, 3, 7]
  • • 这里发生了什么?

    • • map(String::length) 这个操作把 "Alice" 变成了 5"Bob" 变成了 3,以此类推。

    • • 你最终得到了一个由 Integer 值组成的新流

  • • 简单来说:
    map() 对每个元素应用一个一对一的转换函数,并保持流的结构。所以如果你开始时有一个 Stream<T>,经过 map 操作后,你会得到一个 Stream<R>


2. Java 中的 flatMap() 又是什么?

现在,改变游戏规则的家伙来了。
当你的映射函数本身返回的是一个流 (stream) 时,就该用 flatMap() 了。它不会让你得到一个“流中流” (Stream<Stream<T>>) 的结果,而是会把这个嵌套结构**“压平” (flatten)**。

  • • 简单示例:

    t import java.util.Arrays;
    import java.util.List;
    import java.util.stream.Collectors;

    List<String> lines = List.of("apple orange", "banana", "grape melon");// 将每行文字拆分成单词,并合并成一个列表
    List<String> words = lines.stream()// line.split(" ") 返回 String[], Arrays.stream(...) 将其转换为 Stream<String>.flatMap(line -> Arrays.stream(line.split(" "))).collect(Collectors.toList());System.out.println(words); // 输出: [apple, orange, banana, grape, melon]
    ```

    3. 核心区别

    假设你有一个字符串列表,你想把每个单词拆分成单个字符:

    惊呆了没?这就是“压平”的魔力。


    4. 生活中的类比:看 Netflix 剧集 vs. 看所有集

    想象一下这个场景:

    当你使用 map() 时:
    就好比你拿到了一堆电视剧的盒子。每个盒子里都装着那一季的剧集。你得一层层打开每个盒子,才能看到具体的内容。你得到的是 List<List<Episode>>

    当你使用 flatMap() 时:
    就好比你把所有电视剧的盒子都拆开,把所有剧集都倒出来,放进一个巨大的播放列表里。不需要一层层找了,直接从头刷到尾就行。你得到的是 List<Episode>


    5. 用例一:将名字映射为其长度(map() 的完美场景)

    这里,map() 是完美的,因为每个名字(String)都只对应一个长度(Integer),是一对一的转换。

    List<String> names = List.of("John", "Jane", "Jack");List<Integer> lengths = names.stream().map(String::length).collect(Collectors.toList());

    ✅ 输出: [4, 4, 4]
    这里不需要用 flatMap(),因为我们处理的不是嵌套结构。


    6. 用例二:映射到嵌套列表(flatMap 大显身手的地方)

    假设每个员工都有多个电话号码,而你想获取所有员工的所有电话号码。

    class Employee {String name;List<String> phoneNumbers;// 构造函数, getter...public List<String> getPhoneNumbers() { return phoneNumbers; }
    }
    // 假设 employees 是一个 List<Employee>
    List<Employee> employees = /* ... */;// 使用 flatMap 获取所有电话号码
    List<String> allPhoneNumbers = employees.stream().flatMap(emp -> emp.getPhoneNumbers().stream()) // 将每个员工的电话号码列表(List<String>)转换为流(Stream<String>),然后压平.collect(Collectors.toList());

    搞定。现在你得到了一个包含所有员工的所有电话号码的单一列表。
    如果你在这里用 map(),你会得到一个 List<List<String>>(一个列表,其中每个元素又是一个电话号码列表),这意味着你可能得再写个循环来处理它。


    7. 何时使用 map() vs flatMap()

    使用场景

    map()

     (一对一转换)

    flatMap()

     (一对多转换并压平)

    输入Stream<T>Stream<T>
    转换函数T -> RT -> Stream<R>
    输出Stream<R>Stream<R>

     (而不是 Stream<Stream<R>>)

    经验法则: 如果你的转换函数返回的是一个流 (Stream) 或集合 (Collection) → 就用 flatMap()


    8. 内部是如何工作的

    map() 和 flatMap() 都是中间操作 (intermediate operations)。在调用像 collect() 或 forEach() 这样的终端操作 (terminal operation) 之前,它们不会触发任何实际的处理(这被称为惰性求值)。

    在内部:

    你可以把 map() 想象成把一件礼物放进一个盒子里(你最终得到了一堆盒子),而 flatMap() 则是把所有盒子里的礼物都拿出来,放到一个大袋子里(你直接得到了一堆礼物)。


    9. 要避免的常见错误

    错误 1:在处理嵌套流时误用 map()

    // 错误示范
    words.stream().map(word -> word.chars().mapToObj(c -> (char) c)).forEach(System.out::println);

    这会打印出 stream 对象的引用地址,而不是你想要的字符!

    用 flatMap() 修复:

    // 正确示范
    words.stream().flatMap(word -> word.chars().mapToObj(c -> (char) c)).forEach(System.out::println); // 会逐个打印出 'H', 'i', 'B', 'y', 'e'

    10. 面试视角:如何解释 flatMap()

    很多面试官会问:
    map() 和 flatMap() 有什么区别?”

    这是一个黄金回答:

    map() 对流中的每个元素进行一对一的转换,而 flatMap() 用于每个元素会映射出多个值(例如,一个流或集合),并且我们希望最终得到一个扁平化的单一结果流时。例如,把一个包含句子的流拆分成一个包含所有单词的流,或者合并多个嵌套的列表。”

    再随口抛出一个例子,你就稳了。


    11. 总结:这不仅是语法——更是一种哲学

    乍一看,flatMap() 像是 map() 的一个花哨版本,但它实际上是函数式编程中的一个基本概念。

    在实际应用中:

    精通 flatMap() 意味着你深刻理解你的数据结构,并且知道如何高效地去塑造它。而这正是让你成为一名强大的 Java 开发者的原因——不仅仅是知道一个方法能做什么,而是知道为什么以及何时使用它。


    12. 彩蛋:Optional 和 CompletableFuture 中的 FlatMap

    这个“压平”的思想也存在于其他现代 Java API 中。

    • • Optional:
      Optional<String> maybeName = Optional.of("Alice");
      // 如果用 map: Optional.of(Optional.of(name.length())) -> Optional<Optional<Integer>>
      // 用 flatMap:
      Optional<Integer> maybeLength = maybeName.flatMap(name -> Optional.of(name.length())); // 返回 Optional<Integer>
      flatMap 干净地解开了嵌套的 Optional
    • • CompletableFuture:
      CompletableFuture<String> future = CompletableFuture.completedFuture("Hi");// thenApply 会返回 CompletableFuture<CompletableFuture<Integer>>
      // thenCompose (即 flatMap) 返回 CompletableFuture<Integer>
      CompletableFuture<Integer> lengthFuture = future.thenCompose(msg -> CompletableFuture.completedFuture(msg.length()));
      thenCompose() 就是 CompletableFuture 版本的 flatMap()
    • • 当存在一对一的转换时,map() 是完美的。

    • • 当存在一对多的转换或需要处理嵌套数据时,flatMap() 就是王道。

    • • map() 只是简单地包装输出。

    • • flatMap() 则会**“解包”合并**嵌套的元素。

    • • 你有一份 Netflix 电视剧清单(一个 List<Series>)。

    • • 每个电视剧对象(Series)里面都包含一个剧集列表List<Episode>)。

    • • 使用 map()
      import java.util.List;
      import java.util.stream.Stream;List<String> words = List.of("Hi", "Bye");// 尝试用 map 将每个单词转为字符流
      Stream<Stream<Character>> resultWithMap = words.stream().map(word -> word.chars().mapToObj(c -> (char) c));// resultWithMap 的类型是 Stream<Stream<Character>>
      结果: Stream<Stream<Character>> —— 一个包含多个流的流!就像 [ Stream('H', 'i'), Stream('B', 'y', 'e') ]
    • • 使用 flatMap()
      Stream<Character> resultWithFlatMap = words.stream().flatMap(word -> word.chars().mapToObj(c -> (char) c));// resultWithFlatMap 的类型是 Stream<Character>
      结果: Stream<Character> —— 一个单一、干净的字符流!就像 ['H', 'i', 'B', 'y', 'e']
    • • 每一行文字 (line) 都被 split(" ") 拆分成了一个单词数组,然后 Arrays.stream() 将其转换为一个单词流 (Stream<String>)。

    • • 如果用 map,你会得到一个 Stream<Stream<String>>

    • • 但 flatMap() 会接收每个由 line 生成的小流,并将它们压平合并成一个单一的大流。

    • • 这里发生了什么?

    • • 简单来说:
      flatMap() 将每个元素映射成一个,然后将所有这些小流合并(压平)成一个单一的流

http://www.lryc.cn/news/605723.html

相关文章:

  • H.266 vs H.265/AV1/H.264:从工程落地看下一代视频系统的技术演进
  • 前端核心技术Node.js(五)——Mongodb、Mongoose和接口
  • Web3:在 VSCode 中基于 Foundry 快速构建 Solidity 智能合约本地开发环境
  • 硬核技术协同:x86 生态、机密计算与云原生等技术如何为产业数字化转型筑底赋能
  • 云原生环境 DDoS 防护:容器化架构下的流量管控与弹性应对
  • 对git 熟悉时,常用操作
  • Java学习第九十一部分——OkHttp
  • MongoDB用户认证authSource
  • 微服务架构技巧篇——接口类设计技巧
  • 智能交通顶刊TITS论文分享|跨区域自适应车辆轨迹预测:TRACER框架攻克域偏移难题!
  • PageOffice实现文档并发控制
  • 2025年人形机器人动捕技术研讨会将在本周四召开
  • JVM面试通关指南:内存区域、类加载器、双亲委派与GC算法全解析
  • 暑期算法训练.11
  • wpf之ControlTemplate
  • RabbitMQ 消费者确认 (Ack/Nack) (With Spring Boot)
  • HUD抬头显示器-杂散光测试设备 太阳光模拟器
  • FastGPT + Kymo AI生态创新平台,搭建企业智能化知识管理
  • TTS语音合成|GPT-SoVITS语音合成服务器部署,实现http访问
  • 自动驾驶控制算法——PID算法
  • Linux进程创建,终止与等待
  • 运作管理学习笔记1-运作管理基础
  • Docker 初学者需要了解的几个知识点 (五):建容器需要进一步了解的概念
  • 蓝牙 BR/EDR 与 BLE PHY
  • c# net6.0+ 安装中文智能提示
  • JSX语法
  • LPC2132GPIO
  • elk部署加日志收集
  • mac环境配置rust
  • CentOS 7 上使用 Docker 安装 Jenkins 完整教程