一、前言
在前面文章中,我们梳理了 JDK 8 到 11 对 Stream 的 “增量式优化”:JDK 9 解决了空值和有序流筛选问题,JDK 10-11 简化了集合收集操作。而 JDK 16(2021 年 3 月)和 17(2021 年 9 月,LTS)则带来了 Stream 的 “重大增强”—— 不再是小修小补,而是针对核心场景的效率和简洁性升级:
-
JDK 16:新增 mapMulti() 方法,替代 flatMap 解决 “元素拆分” 场景的冗余问题;
-
JDK 17:新增 Collectors.teeing() 方法,解决 “一次遍历多结果聚合” 的痛点;
-
额外优化: toMap() 等收集器的空安全和重复键处理更友好。
这些特性让 Stream 从 “能解决问题” 升级为 “优雅解决问题”,尤其在复杂业务场景下,能大幅减少代码量和性能损耗。本文会逐一拆解这些增强点,通过 “JDK 8 痛点 → JDK 16/17 解决方案 → 实战对比” 的逻辑,让你掌握这些核心新特性。
二、JDK16-17为何是Stream的质变版本
JDK 16 是短期支持版本,核心是 “特性尝鲜”;JDK 17 是继 JDK 8、11 后的第三个 LTS 版本,将 JDK 16 的实用特性正式固化。两者对 Stream 的增强有个共同目标:减少 Stream 遍历次数,简化复杂映射 / 聚合逻辑 —— 这也是 JDK 8 Stream 最核心的痛点之一(复杂场景下要么嵌套冗余,要么多次遍历)。
三、JDK16核心增强:mapMulti
mapMulti() 是 JDK 16 为 Stream 新增的核心方法,解决了 JDK 8 flatMap 在 “元素拆分 / 多元素映射” 场景下的冗余和性能问题。
1、JDK 8 flatMap 的痛点
flatMap 的核心作用是 “将每个元素映射为一个 Stream,再合并为一个 Stream”,但在实际使用中存在两个痛点:
代码冗余:即使只需要发射 1-2 个元素,也必须返回一个 Stream(如 Stream.of() / Collections.singletonStream() );
性能损耗:频繁创建小 Stream 会带来额外的对象创建和销毁开销;
嵌套复杂:多层 flatMap 嵌套时,代码可读性极差。
痛点示例:将订单拆分为订单项
需求:一个 Order 对象包含多个 OrderItem ,需要从订单列表中提取所有订单项。
// JDK 8 flatMap 写法(冗余)
import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;
import java.util.stream.Stream;
public class FlatMapJdk8Demo {
// 订单项
static class OrderItem {
private String productName;
private int quantity;
public OrderItem(String productName, int quantity) {
this.productName = productName;
this.quantity = quantity;
}
@Override
public String toString() {
return productName + "(" + quantity + ")";
}
}
// 订单
static class Order {
private String orderId;
private List<OrderItem> items;
public Order(String orderId, List<OrderItem> items) {
this.orderId = orderId;
this.items = items;
}
public List<OrderItem> getItems() { return items; }
}
public static void main(String[] args) {
List<Order> orders = Arrays.asList(
new Order("O1", Arrays.asList(new OrderItem("手机", 1), new OrderItem("耳机", 2))),
new Order("O2", Arrays.asList(new OrderItem("电脑", 1))),
new Order("O3", null) // 空订单项
);
// JDK 8 flatMap 提取所有订单项(需处理 null,代码冗余)
List<OrderItem> allItems = orders.stream()
.flatMap(order -> {
// 步骤 1:处理 null
if (order.getItems() == null) {
return Stream.empty();
}
// 步骤 2:返回 Stream(即使只有一个元素)
return order.getItems().stream();
})
.collect(Collectors.toList());
System.out.println(allItems); // [手机(1), 耳机(2), 电脑(1)]
}
}
核心问题 :
-
即使只是简单返回一个集合的流,也必须在 flatMap 中写完整的判空 + 返回 Stream 逻辑;
-
如果需要手动发射单个元素(如 “给每个订单新增一个赠品订单项”),还需要 Stream.of(…) 包装,代码更冗余。
2、JDK 16 mapMulti ():更简洁的 “元素发射”
JDK 16 新增的 mapMulti() 方法,核心思想是 “通过 Consumer 直接发射元素,无需包装为 Stream”,方法签名:
<R> Stream<R> mapMulti(BiConsumer<? super T, ? super Consumer<R>> mapper)
-
T:原 Stream 元素类型;
-
R:目标 Stream 元素类型;
-
mapper:一个双消费者,第一个参数是原元素,第二个参数是 “元素发射器”,直接调用 accept() 即可发射元素。
3、实战对比:用 mapMulti 重构上面的订单拆分需求
// JDK 16 mapMulti 写法(简洁)
import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;
public class MapMultiJdk16Demo {
// 复用 Order/OrderItem 类,此处省略
public static void main(String[] args) {
List<Order> orders = Arrays.asList(
new Order("O1", Arrays.asList(new OrderItem("手机", 1), new OrderItem("耳机", 2))),
new Order("O2", Arrays.asList(new OrderItem("电脑", 1))),
new Order("O3", null)
);
// JDK 16 mapMulti 提取所有订单项(无需返回 Stream,直接发射)
List<OrderItem> allItems = orders.stream()
.<OrderItem>mapMulti((order, emitter) -> {
// 步骤 1:处理 null
if (order.getItems() == null) {
return;
}
// 步骤 2:直接发射元素,无需包装 Stream
for (OrderItem item : order.getItems()) {
emitter.accept(item);
}
})
.collect(Collectors.toList());
System.out.println(allItems); // [手机(1), 耳机(2), 电脑(1)]
}
}
核心简化点 :
无需返回 Stream,直接通过 emitter.accept() 发射元素;
处理 null 时直接 return 即可,无需返回 Stream.empty() ;
减少了小 Stream 对象的创建,性能比 flatMap 提升约 10%-20%(大数据量场景更明显)。
4、进阶场景:手动发射多个元素
需求:给每个订单项新增一个 “赠品” 元素(如买手机送手机壳),用 mapMulti 实现更优雅:
// JDK 16 mapMulti 手动发射多个元素
List<OrderItem> itemsWithGift = orders.stream()
.<OrderItem>mapMulti((order, emitter) -> {
if (order.getItems() == null) return;
for (OrderItem item : order.getItems()) {
emitter.accept(item); // 发射原订单项
// 手动发射赠品元素
if ("手机".equals(item.productName)) {
emitter.accept(new OrderItem("手机壳", 1));
}
}
})
.collect(Collectors.toList());
System.out.println(itemsWithGift); // [手机(1), 手机壳(1), 耳机(2), 电脑(1)]
如果用 JDK 8 flatMap 实现这个需求,需要嵌套 Stream.of(item, gift) ,代码嵌套层级更深,可读性差。
5、mapMulti vs flatMap 核心对比
|
特性 |
flatMap (JDK 8) |
mapMulti (JDK 16+) |
|
元素发射方式 |
返回 Stream 间接发射 |
通过 Consumer 直接发射 |
|
空值处理 |
需返回 Stream.empty () |
直接 return 即可 |
|
性能 |
频繁创建小 Stream,开销高 |
无额外对象创建,性能更高 |
|
代码可读性 |
嵌套深,冗余 |
线性逻辑,简洁 |
|
适用场景 |
简单的 “集合转 Stream” 场景 |
复杂的元素拆分、手动发射元素场景 |
注意:mapMulti 并非完全替代 flatMap—— 简单场景(如 stream.flatMap(List::stream) )用 flatMap 更简洁,复杂场景(手动发射、多条件拆分)用 mapMulti 更优。
四、JDK17核心增强:Collectors.teeing
这是 JDK 17 最实用的 Stream 增强,解决了 JDK 8 中 “一次遍历需要获取多个聚合结果” 时,必须多次遍历流的痛点。
1、JDK 8 的痛点:多结果聚合需要多次遍历
在 JDK 8 中,如果需要同时获取 Stream 的多个聚合结果(如最大值、最小值、平均值),只能:
多次遍历 Stream(性能损耗);
手动维护临时变量(代码冗余,且并行流中线程不安全)。
痛点示例:一次遍历获取列表的最大值和最小值
// JDK 8 实现(多次遍历,性能差)
import java.util.Arrays;
import java.util.List;
import java.util.Optional;
public class MultiResultJdk8Demo {
public static void main(String[] args) {
List<Integer> numbers = Arrays.asList(1, 3, 5, 7, 9, 2, 4, 6, 8);
// 第一次遍历:获取最大值
Optional<Integer> max = numbers.stream().max(Integer::compare);
// 第二次遍历:获取最小值
Optional<Integer> min = numbers.stream().min(Integer::compare);
System.out.println("最大值:" + max.get() + ",最小值:" + min.get()); // 9, 1
}
}
核心问题 :
-
大数据量场景下,多次遍历会带来显著的性能损耗;
-
如果是 “消耗型数据源”(如 IO 流生成的 Stream),无法多次遍历(Stream 只能消费一次)。
2、JDK 17 teeing ():一次遍历获取多结果
JDK 17 新增的 Collectors.teeing() 方法,允许你同时执行两个收集器,然后将结果合并,方法签名:
static <T, R1, R2, R> Collector<T, ?, R> teeing(
Collector<? super T, ?, R1> downstream1,
Collector<? super T, ?, R2> downstream2,
BiFunction<? super R1, ? super R2, R> merger
)
-
downstream1/ downstream2 :两个并行执行的收集器;
-
merger:结果合并函数,将两个收集器的结果合并为最终结果。
3、实战对比:用 teeing 一次获取最大值和最小值
// JDK 17 teeing 写法(一次遍历,性能优)
import java.util.Arrays;
import java.util.List;
import java.util.Comparator;
import java.util.stream.Collectors;
// 封装最大值和最小值的结果类
class MinMaxResult {
private int min;
private int max;
public MinMaxResult(int min, int max) {
this.min = min;
this.max = max;
}
@Override
public String toString() {
return "最小值:" + min + ",最大值:" + max;
}
}
public class TeeingJdk17Demo {
public static void main(String[] args) {
List<Integer> numbers = Arrays.asList(1, 3, 5, 7, 9, 2, 4, 6, 8);
// JDK 17 teeing 一次遍历获取最大值和最小值
MinMaxResult result = numbers.stream()
.collect(Collectors.teeing(
// 收集器 1:获取最小值
Collectors.minBy(Comparator.naturalOrder()),
// 收集器 2:获取最大值
Collectors.maxBy(Comparator.naturalOrder()),
// 合并结果:将两个 Optional 转换为 MinMaxResult
(minOpt, maxOpt) -> new MinMaxResult(
minOpt.orElse(0),
maxOpt.orElse(0)
)
));
System.out.println(result); // 最小值:1,最大值:9
}
}
核心优势 :
仅遍历一次 Stream,性能提升 50% 以上(大数据量场景);
代码简洁,无需维护临时变量;
支持并行流,线程安全(收集器本身是线程安全的)。
4、进阶场景:一次遍历获取多个统计值
需求:获取列表的总和、平均值、计数,用 teeing 嵌套实现:
// JDK 17 teeing 嵌套获取多个统计值
import java.util.Arrays;
import java.util.DoubleSummaryStatistics;
import java.util.List;
import java.util.stream.Collectors;
class MultiStatsResult {
private long count;
private int sum;
private double avg;
public MultiStatsResult(long count, int sum, double avg) {
this.count = count;
this.sum = sum;
this.avg = avg;
}
@Override
public String toString() {
return "计数:" + count + ",总和:" + sum + ",平均值:" + avg;
}
}
public class TeeingAdvancedDemo {
public static void main(String[] args) {
List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5);
MultiStatsResult stats = numbers.stream()
.collect(Collectors.teeing(
// 收集器 1:获取计数和总和
Collectors.teeing(
Collectors.counting(),
Collectors.summingInt(Integer::intValue),
(count, sum) -> new Object() {
long count = count;
int sum = sum;
}
),
// 收集器 2:获取平均值
Collectors.averagingInt(Integer::intValue),
// 合并最终结果
(countSum, avg) -> new MultiStatsResult(
countSum.count,
countSum.sum,
avg
)
));
System.out.println(stats); // 计数:5,总和:15,平均值:3.0
}
}
提示:实际开发中,简单统计可直接用 Collectors.summarizingInt() (返回 IntSummaryStatistics ),但 teeing 的优势在于支持任意自定义收集器的组合(如同时分组和求和)。
五、JDK17其他增强
除了 teeing (),JDK 17 还对 Collectors.toMap() 做了细节优化,解决了 JDK 8 中 “重复键必抛异常” 的痛点(虽然 JDK 8 也可通过合并函数处理,但 JDK 17 让默认行为更友好)。
1、JDK 8 toMap () 的痛点:重复键直接抛异常
// JDK 8 toMap 重复键抛异常
import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;
public class ToMapJdk8Demo {
static class User {
private String id;
private String name;
public User(String id, String name) {
this.id = id;
this.name = name;
}
public String getId() { return id; }
public String getName() { return name; }
}
public static void main(String[] args) {
List<User> users = Arrays.asList(
new User("1", "Alice"),
new User("1", "Bob"), // 重复 ID
new User("2", "Charlie")
);
// JDK 8 不指定合并函数,抛 IllegalStateException
try {
users.stream().collect(Collectors.toMap(User::getId, User::getName));
} catch (IllegalStateException e) {
System.out.println("重复键异常:" + e.getMessage());
}
// JDK 8 必须指定合并函数
var userMap = users.stream().collect(Collectors.toMap(
User::getId,
User::getName,
(oldVal, newVal) -> newVal // 保留新值
));
System.out.println(userMap); // {1=Bob, 2=Charlie}
}
}
2、JDK17优化: 更友好的默认行为 + 空安全
JDK 17 并未修改 toMap () 的核心逻辑,但做了两点优化:
异常信息更清晰:明确指出重复的键值,便于调试;
空值处理优化:允许值为 null(JDK 8 中值为 null 会抛 NPE,JDK 17 修复了这个问题);
新增 Collectors.toUnmodifiableMap() 的重载方法,支持更简洁的重复键处理。
// JDK 17 toMap 空安全 & 清晰异常
import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;
public class ToMapJdk17Demo {
// 复用 User 类,此处省略
public static void main(String[] args) {
List<User> users = Arrays.asList(
new User("1", "Alice"),
new User("1", null), // 值为 null
new User("2", "Charlie")
);
// JDK 17 支持值为 null 的 toMap
var userMap = users.stream().collect(Collectors.toMap(
User::getId,
User::getName,
(oldVal, newVal) -> newVal
));
System.out.println(userMap); // {1=null, 2=Charlie}
// JDK 17 toUnmodifiableMap 重复键异常信息更清晰
try {
users.stream().collect(Collectors.toUnmodifiableMap(User::getId, User::getName));
} catch (IllegalStateException e) {
// 异常信息:Duplicate key 1 (attempted merging values Alice and null)
System.out.println("JDK 17 异常信息:" + e.getMessage());
}
}
}
六、JDK8 vs JDK17 完整业务场景对比
需求:从订单列表中,提取所有订单项,同时统计订单项的总数量、最高单价、最低单价,要求代码简洁、仅遍历一次、空安全。
1、JDK 8 实现(冗余 + 多次遍历)
// JDK 8 实现
import java.util.*;
import java.util.stream.Collectors;
public class BusinessJdk8Demo {
// 订单项(新增单价字段)
static class OrderItem {
private String productName;
private int quantity;
private double price;
public OrderItem(String productName, int quantity, double price) {
this.productName = productName;
this.quantity = quantity;
this.price = price;
}
// getter
public int getQuantity() { return quantity; }
public double getPrice() { return price; }
}
// 订单
static class Order {
private String orderId;
private List<OrderItem> items;
public Order(String orderId, List<OrderItem> items) {
this.orderId = orderId;
this.items = items;
}
public List<OrderItem> getItems() { return items; }
}
// 统计结果
static class OrderStats {
private List<OrderItem> allItems;
private int totalQuantity;
private double maxPrice;
private double minPrice;
public OrderStats(List<OrderItem> allItems, int totalQuantity, double maxPrice, double minPrice) {
this.allItems = allItems;
this.totalQuantity = totalQuantity;
this.maxPrice = maxPrice;
this.minPrice = minPrice;
}
@Override
public String toString() {
return "总数量:" + totalQuantity + ",最高单价:" + maxPrice + ",最低单价:" + minPrice + "\\n订单项:" + allItems;
}
}
public static void main(String[] args) {
List<Order> orders = Arrays.asList(
new Order("O1", Arrays.asList(new OrderItem("手机", 1, 5999.0), new OrderItem("耳机", 2, 199.0))),
new Order("O2", Arrays.asList(new OrderItem("电脑", 1, 8999.0))),
new Order("O3", null)
);
// 步骤 1:提取所有订单项(第一次遍历)
List<OrderItem> allItems = orders.stream()
.flatMap(order -> order.getItems() == null ? Stream.empty() : order.getItems().stream())
.collect(Collectors.toList());
// 步骤 2:统计总数量(第二次遍历)
int totalQuantity = allItems.stream()
.mapToInt(OrderItem::getQuantity)
.sum();
// 步骤 3:统计最高单价(第三次遍历)
double maxPrice = allItems.stream()
.mapToDouble(OrderItem::getPrice)
.max()
.orElse(0.0);
// 步骤 4:统计最低单价(第四次遍历)
double minPrice = allItems.stream()
.mapToDouble(OrderItem::getPrice)
.min()
.orElse(0.0);
OrderStats stats = new OrderStats(allItems, totalQuantity, maxPrice, minPrice);
System.out.println(stats);
}
}
2、JDK 17 实现(简洁 + 一次遍历)
// JDK 17 实现
import java.util.Arrays;
import java.util.Comparator;
import java.util.List;
import java.util.stream.Collectors;
public class BusinessJdk17Demo {
// 复用 OrderItem/Order/OrderStats 类,此处省略
public static void main(String[] args) {
List<Order> orders = Arrays.asList(
new Order("O1", Arrays.asList(new OrderItem("手机", 1, 5999.0), new OrderItem("耳机", 2, 199.0))),
new Order("O2", Arrays.asList(new OrderItem("电脑", 1, 8999.0))),
new Order("O3", null)
);
// JDK 17:mapMulti + teeing 一次遍历完成所有操作
OrderStats stats = orders.stream()
// 步骤 1:mapMulti 提取所有订单项
.<OrderItem>mapMulti((order, emitter) -> {
if (order.getItems() == null) return;
for (OrderItem item : order.getItems()) {
emitter.accept(item);
}
})
// 步骤 2:teeing 一次遍历获取多结果
.collect(Collectors.teeing(
// 收集器 1:获取所有订单项
Collectors.toList(),
// 收集器 2:嵌套 teeing 获取总数量、最高单价、最低单价
Collectors.teeing(
Collectors.summingInt(OrderItem::getQuantity),
Collectors.teeing(
Collectors.maxBy(Comparator.comparingDouble(OrderItem::getPrice)),
Collectors.minBy(Comparator.comparingDouble(OrderItem::getPrice)),
(maxOpt, minOpt) -> new Object() {
double maxPrice = maxOpt.map(OrderItem::getPrice).orElse(0.0);
double minPrice = minOpt.map(OrderItem::getPrice).orElse(0.0);
}
),
(totalQty, priceStats) -> new Object() {
int totalQuantity = totalQty;
double maxPrice = priceStats.maxPrice;
double minPrice = priceStats.minPrice;
}
),
// 合并最终结果
(allItems, statsData) -> new OrderStats(
allItems,
statsData.totalQuantity,
statsData.maxPrice,
statsData.minPrice
)
));
System.out.println(stats);
}
}
核心优势 :
仅遍历一次 Stream,性能大幅提升;
代码逻辑线性化,无多次遍历的冗余;
结合 mapMulti 和 teeing,完美解决 JDK 8 的痛点。
七、总结
JDK 16 的 mapMulti() 是 flatMap 的 “升级版”:直接通过发射器发射元素,无需包装 Stream,代码更简洁、性能更高,适合复杂的元素拆分场景;
JDK 17 的 Collectors.teeing() 解决了 “多结果聚合” 的核心痛点:一次遍历即可获取多个聚合结果,避免多次遍历的性能损耗;
JDK 17 对 toMap() 的细节优化,让重复键和空值处理更友好,降低了开发中的踩坑概率;
JDK 16-17 的增强让 Stream 从 “基础数据处理” 升级为 “复杂业务场景处理”,是 JDK 17 成为主流 LTS 版本的重要原因。
下一篇文章,我们会聚焦 “JDK 8 vs 后续版本 Stream 常用场景代码对比”,通过更多实战场景,帮你掌握不同版本下的最佳写法,同时讲解版本兼容和最佳实践。


