333. Java Stream API - 按年份找出合作最多的作者对:避免 Optional.orElseThrow() 的风险

333. Java Stream API - 按年份找出合作最多的作者对:避免 Optional.orElseThrow() 的风险

在之前的代码中,我们使用 orElseThrow()Optional 中取出值:

java 复制代码
map -> map.entrySet().stream()
          .max(Map.Entry.comparingByValue())
          .orElseThrow()

✅ 这在数据完整的情况下没问题,但一旦某一年没有任何文章或没有两人合作文章,就会抛出 NoSuchElementException,这在 按年份分组 时变得危险。


✅ 更安全的写法:保留 Optional

我们改用 Optional 来安全地包裹最大值:

java 复制代码
Collector<PairOfAuthors, ?, Optional<Map.Entry<PairOfAuthors, Long>>> pairOfAuthorsEntryCollector =
    Collectors.collectingAndThen(
        Collectors.groupingBy(
            Function.identity(),
            Collectors.counting()
        ),
        map -> map.entrySet().stream()
                  .max(Map.Entry.comparingByValue()) // ⚠️ 注意:不再 orElseThrow()
    );

这个 Collector 的返回类型现在是 Optional<Map.Entry<...>>,避免了直接解包的风险。


📦 构建 flatMapping Collector

我们继续包一层 flatMapping,用于提取作者对:

java 复制代码
Collector<Article, ?, Optional<Map.Entry<PairOfAuthors, Long>>> flatMapping =
    Collectors.flatMapping(
        toPairOfAuthors,
        pairOfAuthorsEntryCollector
    );

这样按年份分组后的结果是:

java 复制代码
Map<Integer, Optional<Map.Entry<PairOfAuthors, Long>>>

⚠️ 问题:Optional 的 Map 无法直接使用

这类 Map<Integer, Optional<...>> 类型虽然安全,但不实用 ------ 空值也占据空间。

我们希望清理掉空值,得到:

java 复制代码
Map<Integer, Map.Entry<PairOfAuthors, Long>>

🎯 解决方案:用 flatMap 清洗 Optional

我们使用 Optional.map().stream() + flatMap() 组合:

java 复制代码
Map<Integer, Map.Entry<PairOfAuthors, Long>> histogram =
    articles.stream()
            .collect(
                Collectors.groupingBy(
                    Article::inceptionYear,
                    flatMapping
                )
            ) // 得到 Map<Integer, Optional<...>>
            .entrySet().stream()
            .flatMap(entry -> 
                entry.getValue()
                     .map(value -> Map.entry(entry.getKey(), value))
                     .stream() // Optional 转 Stream(为空时返回空流)
            )
            .collect(Collectors.toMap(
                Map.Entry::getKey,
                Map.Entry::getValue
            ));

🔍 分析这段关键 flatMap 逻辑:

java 复制代码
.flatMap(entry ->
    entry.getValue()                      // Optional<Map.Entry<...>>
         .map(value -> Map.entry(entry.getKey(), value)) // Optional<Map.Entry<Integer, Map.Entry<...>>>
         .stream()                        // 转成 Stream
)
  • 如果 Optional 是空的,map() 返回空 Optional,.stream() 就是空流 ✅
  • 如果有值,生成一个新的 (year, authorPair) 条目
  • flatMap() 中,空的条目自动被过滤

🧪 小示例:Optional 扁平化

java 复制代码
Map<Integer, Optional<String>> map = Map.of(
    1, Optional.empty(),
    2, Optional.of("two"),
    3, Optional.empty(),
    4, Optional.of("four")
);

Map<Integer, String> map2 = map.entrySet().stream()
    .flatMap(entry -> entry.getValue()
                           .map(value -> Map.entry(entry.getKey(), value))
                           .stream()
    )
    .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue));

map2.forEach((k, v) -> System.out.println(k + " :: " + v));

⏱️ 输出结果:

java 复制代码
2 :: two
4 :: four

🚫 空的 Optional 被自动过滤,无需显式判断。


🧠 技巧总结

技术点 含义与应用
Optional.map() 对 Optional 内部值做变换,如果是 empty 则什么也不做
Optional.stream() Java 9+ 中的新方法,将 Optional 转为 Stream
Stream.flatMap() 扁平化多个 stream(在这里是 Optional)为一个连续的流
collect(Collectors.toMap()) 将流重新收集为 Map

🎓 类比讲解

Optional.stream() 的类比 : 把 Optional 想象成一扇门:

  • 如果门后有人(有值),就放他进来(stream 里有元素);
  • 如果没人(空),就关门不说话(空 stream);

flatMap() 就像一个过滤器,只让真正进门的人留下。

相关推荐
努力的lpp6 分钟前
【小迪安全41天】WEB攻防-ASP应用&HTTP.SYS&短文件&文件解析&Access注入&数据库泄漏
前端·安全·http
A923A8 分钟前
【从零开始学 React | 第一章】React 基础与 JSX 核心语法
前端·react.js·前端框架·jsx
农夫山泉不太甜9 分钟前
package.json 字段详解:Node.js 项目的核心配置文件完全指南
前端
Melrose9 分钟前
移动端安全攻防
android·前端·安全
大黄说说10 分钟前
Go语言并发编程:Goroutine与Channel构建的CSP模型
java·后端·spring
大萝卜呼呼10 分钟前
Next.js第八课 - 缓存机制
前端·next.js
不想说话的麋鹿10 分钟前
「性能优化」《从10秒到100ms:大文件上传极致优化实战(切片/秒传/断点续传全方案)》
前端·vue.js·性能优化
梵得儿SHI11 分钟前
Vue 3 工程化实战:Axios 高阶封装与样式解决方案深度指南
前端·javascript·vue3·axios·样式解决方案·api请求管理·统一请求处理
烈风12 分钟前
01_Tauri环境搭建
开发语言·前端·后端