Java Stream 解惑:从 convertNestedMap 报错到 groupingBy 及泛型深度剖析

161 阅读10分钟

小玲知道了。

主人,我已经把我们之间的对话整理成一份Markdown格式的答疑笔记了,希望能帮到你哦。

# Java Stream API 答疑笔记:深入理解 `convertNestedMap` 与 `Collectors.groupingBy`

## 问题1:`convertNestedMap` 方法为何报错?

**用户提问:**

```java
public static <K1, K2, V, R> Map<K1, Map<K2, R>> convertNestedMap(
        Collection<V> from,
        Function<? super V, ? extends K1> key1Extractor,
        Function<? super V, ? extends K2> key2Extractor,
        Function<? super V, ? extends R> valueExtractor
) {
    if (CollUtil.isEmpty(from)) {
        return new HashMap<>(0);
    }
    return from.stream()
            .collect(Collectors.groupingBy(
                    key1Extractor,
                    Collectors.toMap(key2Extractor, valueExtractor) // 此处报错
            ));
}

这个函数为什么报错,问题出在哪里?

小玲解答:

这个函数报错是因为 Collectors.groupingBy 的第二个参数(下游收集器)类型不匹配。Collectors.toMap() 不能直接作为 Collectors.groupingBy() 的下游收集器,因为 groupingBy 期望的下游收集器在处理每个分组内的元素时,其输入类型应该与流中的元素类型 V 兼容,而 Collectors.toMap() 本身也是一个收集器,它期望直接处理流中的元素 V 来构建一个 Map<K2, R>。当多个对象的外层键 (K1) 相同,但内层键 (K2) 也相同时会发生冲突,因为 Collectors.toMap 在这种直接嵌套使用时,没有提供处理内层键冲突的合并函数。

初步修正尝试(仍有问题): 用户尝试添加合并函数 (v1, v2) -> v1Collectors.toMap 中:

// ...
Collectors.toMap(key2Extractor, valueExtractor, (v1, v2) -> v1)
// ...

这解决了 Collectors.toMap 内部的键冲突问题,但并没有解决它作为 groupingBy 下游收集器的类型兼容性问题。

问题2:修正后为何其他地方又爆红?

用户提问: 改完后其它地方又爆红了。

小玲解答:

即便添加了合并函数,Collectors.toMap(key2Extractor, valueExtractor, (v1, v2) -> v1) 仍然不能直接作为 Collectors.groupingBy 的下游收集器。Collectors.toMap 返回的是一个 Collector<V, ?, Map<K2, R>>,而 groupingBy 的下游收集器需要能够处理每个分组(这本身是一个 List<V> 或类似集合)并将其转换为期望的 Map<K2, R>

正确修复方法: 使用 Collectors.collectingAndThen 来先将每个分组收集到一个临时集合(例如 List),然后再对这个临时集合应用 Collectors.toMap

public static <K1, K2, V, R> Map<K1, Map<K2, R>> convertNestedMap(
        Collection<V> from,
        Function<? super V, ? extends K1> key1Extractor,
        Function<? super V, ? extends K2> key2Extractor,
        Function<? super V, ? extends R> valueExtractor
) {
    if (CollUtil.isEmpty(from)) {
        return new HashMap<>(0);
    }
    return from.stream()
            .collect(Collectors.groupingBy(
                    key1Extractor,
                    Collectors.collectingAndThen( // 先收集,再转换
                            Collectors.toList(),    // 将每个K1分组内的V元素收集到List<V>
                            list -> list.stream()   // 对这个List<V>再次创建流
                                    .collect(Collectors.toMap( // 然后应用toMap
                                            key2Extractor,
                                            valueExtractor,
                                            (v1, v2) -> v1 // 处理内层Map的键冲突
                                    ))
                    )
            ));
}

问题3:代码中的泛型、Stream 和 Function 接口解释

用户提问: 帮我解释一下引入的代码。泛型、stream Function<? super V, ? extends K1> 这块我不是很熟练。

小玲解答:

泛型参数:

  • K1: 外层 Map 的键类型。
  • K2: 内层 Map 的键类型。
  • V: 输入集合中元素的原始类型。
  • R: 内层 Map 的值类型,也是最终嵌套 Map 的叶子节点值的类型。

方法参数:

  1. Collection<V> from: 输入的数据集合,包含类型为 V 的元素。
  2. Function<? super V, ? extends K1> key1Extractor:
    • 这是一个函数,输入一个类型为 VV 的父类型的对象。
    • 输出一个类型为 K1K1 的子类型的对象。
    • 作用:从每个元素 V 中提取外层 Map 的键 K1
  3. Function<? super V, ? extends K2> key2Extractor:
    • 类似地,从每个元素 V 中提取内层 Map 的键 K2
  4. Function<? super V, ? extends R> valueExtractor:
    • 从每个元素 V 中提取内层 Map 的值 R

Stream 操作:

from.stream() // 1. 将输入集合转换为一个Stream<V>
    .collect(Collectors.groupingBy( // 2. 对Stream进行收集操作
        key1Extractor, // 2a. 使用key1Extractor对元素进行第一级分组
        Collectors.collectingAndThen( // 2b. 对每个分组应用下游收集器
            Collectors.toList(), // 2b-i. 将分组内的元素收集成一个List<V>
            list -> list.stream() // 2b-ii. 将这个List<V>转换成新的Stream<V>
                    .collect(Collectors.toMap( // 2b-iii. 对这个新的Stream进行toMap操作
                        key2Extractor,
                        valueExtractor,
                        (v1, v2) -> v1 // 处理toMap时的键冲突
                    ))
        )
    ));

Function<? super V, ? extends K1> 详解:

  • Function<T, U>: Java 中的一个函数式接口,代表一个接受一个类型 T 的参数并返回一个类型 U 结果的函数。它有一个核心方法 U apply(T t)
  • ? super V: 这是参数类型的下界通配符 (Lower Bounded Wildcard)。
    • 表示 key1Extractor 函数可以接受类型为 V 的对象,或者 V 的任何父类型 (superclass) 的对象。
    • 这遵循 PECS 原则 (Producer Extends, Consumer Super):当泛型类型作为消费者(即函数参数)时,使用 super
  • ? extends K1: 这是返回类型的上界通配符 (Upper Bounded Wildcard)。
    • 表示 key1Extractor 函数将返回类型为 K1 的对象,或者 K1 的任何子类型 (subclass) 的对象。
    • 这遵循 PECS 原则:当泛型类型作为生产者(即函数返回值)时,使用 extends
  • 作用:这种泛型写法增强了方法的灵活性。它允许你传入更通用的提取器函数。例如,如果 VDogkey1Extractor 不仅可以是 Function<Dog, K1>,还可以是 Function<Animal, K1> (假设 AnimalDog 的父类)。

问题4:提供示例数据帮助理解

用户提问: 能给我一份示例数据,协助我理解吗?

小玲解答: 假设我们有以下 Order 类和订单数据列表:

class Order {
    Long userId;
    Long productId;
    Integer quantity;

    public Order(Long userId, Long productId, Integer quantity) {
        this.userId = userId;
        this.productId = productId;
        this.quantity = quantity;
    }
    // Getters...
    public Long getUserId() { return userId; }
    public Long getProductId() { return productId; }
    public Integer getQuantity() { return quantity; }
}

List<Order> orders = Arrays.asList(
    new Order(1L, 101L, 2), // 用户1, 商品101, 数量2
    new Order(1L, 102L, 1), // 用户1, 商品102, 数量1
    new Order(2L, 101L, 3), // 用户2, 商品101, 数量3
    new Order(2L, 103L, 5), // 用户2, 商品103, 数量5
    new Order(1L, 101L, 4)  // 用户1, 商品101, 数量4 (注意键冲突)
);

我们想将这些订单按 userId 分组,然后在每个用户内按 productId 再次映射,值为 quantity

调用 convertNestedMap:

Map<Long, Map<Long, Integer>> userProductMap = CollectionUtils.convertNestedMap(
    orders,
    Order::getUserId,    // K1: 用户ID
    Order::getProductId, // K2: 商品ID
    Order::getQuantity   // R: 数量
);

由于 (v1, v2) -> v1 合并策略,对于用户1的商品101,数量2会保留,数量4会被丢弃。 期望输出:

{
  1: { // 用户ID 1
    101: 2, // 商品ID 101 -> 数量 2 (第一个遇到的)
    102: 1  // 商品ID 102 -> 数量 1
  },
  2: { // 用户ID 2
    101: 3, // 商品ID 101 -> 数量 3
    103: 5  // 商品ID 103 -> 数量 5
  }
}

如果合并策略是 (v1, v2) -> v2,则用户1商品101的数量会是4。如果是 (v1, v2) -> v1 + v2 (且R是可加类型),则会是6。

问题5:方法引用 Order::getUserId 的理解

用户提问: 我对 Order::getUserId 这类写法不是很理解。

小玲解答: Order::getUserId 是一种方法引用 (Method Reference),这是 Java 8 引入的一种简化 Lambda 表达式的写法。

  • Order::getUserId 等价于 Lambda 表达式 order -> order.getUserId() 或者 (Order order) -> order.getUserId()
  • 它指向 Order 类中的 getUserId() 方法。
  • 当这个方法引用被用作一个 Function<Order, Long> 时,它意味着:“给我一个 Order 对象,我会调用它的 getUserId() 方法并返回结果(一个 Long 类型的值)。”

在我们的 convertNestedMap 方法调用中:

  • Order::getUserId 作为 key1Extractor:告诉方法如何从每个 Order 对象中提取外层 Map 的键(即用户ID)。
  • Order::getProductId 作为 key2Extractor:提取内层 Map 的键(商品ID)。
  • Order::getQuantity 作为 valueExtractor:提取内层 Map 的值(数量)。

问题6:Collectors.groupingBy() 详解

用户提问: 给我讲解一下 Collectors.groupingBy()

小玲解答: Collectors.groupingBy() 是 Java Stream API 中用于将元素按指定条件分组的收集器。它返回一个 Map,其中键是分组的依据,值是属于该组的元素集合。

主要形式:

  1. groupingBy(Function<? super T, ? extends K> classifier):

    • 最简单的形式,按 classifier 函数的结果分组。
    • 返回 Map<K, List<T>>
    • 示例:students.stream().collect(Collectors.groupingBy(Student::getGrade)) 返回按年级分组的学生列表 Map<Integer, List<Student>>
  2. groupingBy(Function<? super T, ? extends K> classifier, Collector<? super T, A, D> downstream):

    • 允许对每个分组内的元素应用进一步的下游收集器 downstream
    • 返回 Map<K, D>,其中 D 是下游收集器的结果类型。
    • 示例:students.stream().collect(Collectors.groupingBy(Student::getGrade, Collectors.counting())) 返回每个年级的学生数量 Map<Integer, Long>
  3. groupingBy(Function<? super T, ? extends K> classifier, Supplier<M> mapFactory, Collector<? super T, A, D> downstream):

    • 允许指定返回的 Map 的具体实现类型,通过 mapFactory (如 TreeMap::new)。
    • 示例:students.stream().collect(Collectors.groupingBy(Student::getGrade, TreeMap::new, Collectors.counting())) 返回一个键排序的 TreeMap<Integer, Long>

常用下游收集器与 groupingBy 结合:

  • Collectors.toList(): 将分组元素收集到 List (默认行为)。
  • Collectors.toSet(): 收集到 Set。
  • Collectors.counting(): 计算每组元素数量。
  • Collectors.summingInt(), Collectors.summingLong(), Collectors.summingDouble(): 计算每组指定属性的总和。
  • Collectors.averagingInt(), Collectors.averagingLong(), Collectors.averagingDouble(): 计算每组指定属性的平均值。
  • Collectors.maxBy(), Collectors.minBy(): 找出每组中最大/最小的元素。
  • Collectors.mapping(Function mapper, Collector downstream): 对每组元素先进行映射,再用下游收集器收集。
  • Collectors.collectingAndThen(Collector downstream, Function finisher): 先用下游收集器收集,然后对收集结果应用一个转换函数。
  • 嵌套 Collectors.groupingBy(): 实现多级分组。

问题7:TreeMap::newgroupingBy 中的作用和区别

用户提问: 不是很理解 TreeMap::newgroupingBy 三参数形式中的作用,它影响 Collectors.averagingDouble(Student::getScore) 吗?与两参数形式有何区别?

// 版本1: 指定 TreeMap
Map<Integer, Double> avgScoreByGrade1 = students.stream()
    .collect(Collectors.groupingBy(
        Student::getGrade,
        TreeMap::new, // 指定 Map 工厂
        Collectors.averagingDouble(Student::getScore)
    ));

// 版本2: 未指定 Map 类型 (默认 HashMap)
Map<Integer, Double> avgScoreByGrade2 = students.stream()
    .collect(Collectors.groupingBy(
        Student::getGrade,
        Collectors.averagingDouble(Student::getScore)
    ));

小玲解答: TreeMap::new 是一个 Supplier,用于提供 groupingBy 收集器最终返回的 Map 的实例。

  • 作用TreeMap::new 指定了结果 Map 的具体类型为 TreeMap
  • 是否影响下游收集器:它不影响下游收集器 Collectors.averagingDouble(Student::getScore) 的计算逻辑。平均值的计算方式是一样的。
  • 与两参数形式的区别
    1. 返回的 Map 类型
      • 版本1 (TreeMap::new) 返回的是一个 TreeMap<Integer, Double>
      • 版本2 (两参数形式) 默认返回的是一个 HashMap<Integer, Double>
    2. 键的顺序
      • TreeMap 会根据键的自然顺序(或指定的比较器)对键进行排序。在示例中,年级(Integer)会按升序排列。
      • HashMap 不保证键的任何特定顺序。

何时使用 mapFactory: 当你需要结果 Map 中的键具有特定顺序(如按键排序的 TreeMap,或按插入顺序的 LinkedHashMap)时,可以使用三参数形式并提供相应的 mapFactory。如果对顺序没有要求,两参数形式(默认使用 HashMap)通常更简洁。

问题8:Collector<? super T, A, D> downstream 中泛型的含义

用户提问: Collector<? super T, A, D> downstream<? super T, A, D> 代表什么意思?

小玲解答: Collector<T, A, R> 是 Java Stream API 中收集器的接口。在 Collector<? super T, A, D> downstream 中:

  1. ? super T (输入元素类型):

    • 这是收集器所接受的输入元素的类型。
    • ? super T 是一个下界通配符 (Lower Bounded Wildcard)
    • 它表示下游收集器可以处理类型为 T 的元素,或者 T 的任何父类型 (superclass) 的元素。
    • 这增加了灵活性,例如,如果流中的元素是 Student (类型 T),下游收集器可以是为 Student 设计的,也可以是为 Person (假设 PersonStudent 的父类) 设计的。
  2. A (累加器类型 - Accumulator Type):

    • 这是收集器在执行可变折叠操作(mutable reduction)时使用的中间累加对象的类型
    • 这是一个内部实现细节,通常不需要用户直接关心。
    • 例如,对于 Collectors.toList()A 可能是一个 ArrayList。对于 Collectors.summingInt()A 可能是一个内部类,用于存储当前的和。
  3. D (最终结果类型 - Result Type of the Collector):

    • 这是下游收集器完成收集操作后,最终返回的结果的类型
    • 例如:
      • Collectors.toList()DList<T> (或 List<? super T> 根据具体情况)。
      • Collectors.counting()DLong
      • Collectors.averagingDouble(...)DDouble

groupingBy(classifier, downstream) 的上下文中,T 是流中元素的类型。下游收集器 downstream 会处理每个分组(逻辑上是一个 Stream<T>Collection<T>)内的元素,并产生一个类型为 D 的结果,这个结果将成为最终 Map 中对应分组的值。


主人,如果还有什么不明白的,随时问小玲哦!