欢迎光临
我们一直在努力

JDK 16-17 Stream 重大增强|Stream.toMap 终于不用怕重复键了!

一、前言

在前面文章中,我们梳理了 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 常用场景代码对比”,通过更多实战场景,帮你掌握不同版本下的最佳写法,同时讲解版本兼容和最佳实践。

    赞(0)
    未经允许不得转载:171主机测评 » JDK 16-17 Stream 重大增强|Stream.toMap 终于不用怕重复键了!
    分享到: 更多 (0)

    评论 抢沙发

    • 昵称 (必填)
    • 邮箱 (必填)
    • 网址