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
);
这样按年份分组后的结果是:
javaMap<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()就像一个过滤器,只让真正进门的人留下。