流中收集器中的比较器导致类型推断问题?

Ric*_*ick 4 java generics type-inference language-lawyer java-stream

我有以下简化的示例,以TreeMap从Integer到List的形式将字符串列表分组为类别

public static void main(String[] args)
{
    List<String> list = Arrays.asList("A", "B", "C", "D", "E");

    TreeMap<Integer, List<String>> res = list.stream()
        .collect(Collectors.groupingBy(
            s -> s.charAt(0) % 3,
            () -> new TreeMap<>(Comparator.<Integer>reverseOrder()), // Type required
            Collectors.toList()
        ));

    System.out.println(res);
}
Run Code Online (Sandbox Code Playgroud)

如果我未指定Comparator.reverseOrder()的类型,则代码将无法编译(有关错误,请参见文章底部)。

如果我明确指定TreeMap的类型而不是Comparator.reverseOrder()的类型,则代码可以正常工作。

() -> new TreeMap<Integer, List<String>>(Comparator.reverseOrder()), // Type required
Run Code Online (Sandbox Code Playgroud)

所以:

  • 编译器能够推断出TreeMap的类型
  • 如果编译器知道TreeMap的类型,则它能够推断Comparator的类型。
  • 但是,如果编译器必须推断出TreeMap的类型,则它无法弄清Comparator的类型。

我不明白为什么编译器不能同时推断两种类型。我已经使用Oracle的JDK 1.8.0_191和AdoptOpenJDK的JDK 11.0.1_13进行了测试,结果相同。

这是我不知道的一些限制吗?

Error:(22, 32) java: no suitable method found for groupingBy((s)->s.cha[...]) % 3,()->new Tr[...]er()),java.util.stream.Collector<java.lang.Object,capture#1 of ?,java.util.List<java.lang.Object>>)
    method java.util.stream.Collectors.<T,K>groupingBy(java.util.function.Function<? super T,? extends K>) is not applicable
      (cannot infer type-variable(s) T,K
        (actual and formal argument lists differ in length))
    method java.util.stream.Collectors.<T,K,A,D>groupingBy(java.util.function.Function<? super T,? extends K>,java.util.stream.Collector<? super T,A,D>) is not applicable
      (cannot infer type-variable(s) T,K,A,D
        (actual and formal argument lists differ in length))
    method java.util.stream.Collectors.<T,K,D,A,M>groupingBy(java.util.function.Function<? super T,? extends K>,java.util.function.Supplier<M>,java.util.stream.Collector<? super T,A,D>) is not applicable
      (inferred type does not conform to upper bound(s)
        inferred: java.lang.Object
        upper bound(s): java.lang.Comparable<? super T>,T,java.lang.Object)
Run Code Online (Sandbox Code Playgroud)

Hol*_*ger 6

不幸的是,类型推断具有非常复杂的规范,这使得很难确定特定的奇怪行为是符合规范还是仅是编译器错误。

类型推断有两个众所周知的故意限制。

首先,表达式的目标类型不用于接收方表达式,即方法调用链中。所以当你有一个形式的声明

TargetType x = first.second(…).third(…);
Run Code Online (Sandbox Code Playgroud)

TargetType将使用推断通用类型的third()调用和它的参数表现,而不是second(…)调用。因此,类型推断second(…)只能使用和的独立类型first

这不是问题。由于独立型list的好定义为List<String>,存在推断的结果类型没问题Stream<String>stream()电话,有问题的collect调用链,可以使用目标类型的最后一个方法调用TreeMap<Integer, List<String>>推断类型参数。

第二个限制是关于过载解析。语言设计人员在涉及参数表达的不完整类型之间的循环依赖时,故意进行了削减,这些不完整类型的参数表达式需要先了解实际的目标方法及其类型,然后才能帮助确定正确的调用方法。

这在这里也不适用。当groupingBy重载时,这些方法的参数数量有所不同,这允许在不知道参数类型的情况下选择唯一合适的方法。还可以证明,当我们替换groupingBy为具有预期签名但没有重载的其他方法时,编译器的行为不会改变。


您的问题可以通过使用解决,例如

TreeMap<Integer, List<String>> res = list.stream()
    .collect(Collectors.groupingBy(
        (String s) -> s.charAt(0) % 3,
        () -> new TreeMap<>(Comparator.reverseOrder()),
        Collectors.toList()
    ));
Run Code Online (Sandbox Code Playgroud)

这为分组函数使用了显式类型化的lambda表达式,尽管该表达式实际上并未对映射键的类型有所贡献,但会使编译器找到实际的类型。

如上所述,尽管使用显式类型的lambda表达式而不是隐式类型的表达式可以在方法重载解析上有所不同,但在此不应该将其应用于此处,因为这种特定情况不是重载方法的问题。

足够奇怪的是,甚至以下更改也使编译器错误消失了:

static <X> X dummy(X x) { return x; }
…

TreeMap<Integer, List<String>> res = list.stream()
    .collect(Collectors.groupingBy(
        s -> s.charAt(0) % 3,
        dummy(() -> new TreeMap<>(Comparator.reverseOrder())),
        Collectors.toList()
    ));
Run Code Online (Sandbox Code Playgroud)

在这里,我们没有帮助任何其他显式类型,也没有改变lambda表达式的形式性质,但是,编译器仍然突然正确地推断了所有类型。

该行为似乎与以下事实有关:零参数lambda表达式始终被显式键入。由于我们无法更改零参数lambda表达式的性质,因此创建了以下替代收集器方法进行验证:

public static <T, K, D, A, M extends Map<K, D>>
Collector<T, ?, M> groupingBy(Function<? super T, ? extends K> classifier,
                              Function<Void,M> mapFactory,                                 
                              Collector<? super T, A, D> downstream) {
    return Collectors.groupingBy(classifier, () -> mapFactory.apply(null), downstream);
}
Run Code Online (Sandbox Code Playgroud)

然后,使用隐式类型的lambda表达式作为map factory进行编译时不会出现问题:

TreeMap<Integer, List<String>> res = list.stream()
    .collect(groupingBy(
        s -> s.charAt(0) % 3,
        x -> new TreeMap<>(Comparator.reverseOrder()),
        Collectors.toList()
    ));
Run Code Online (Sandbox Code Playgroud)

而使用显式类型的lambda表达式会导致编译器错误:

TreeMap<Integer, List<String>> res = list.stream()
    .collect(groupingBy(                           // compiler error
        s -> s.charAt(0) % 3,
        (Void x) -> new TreeMap<>(Comparator.reverseOrder()),
        Collectors.toList()
    ));
Run Code Online (Sandbox Code Playgroud)

我认为,即使规范支持此行为,也应予以纠正,因为提供显式类型的含义绝不应该是类型推断变得比没有条件恶化。对于零参数lambda表达式尤其如此,我们不能将其转换为隐式类型的表达式。

它还没有解释为什么将所有参数都转换为显式类型的lambda表达式也可以消除编译器错误。

  • 99%的人(我也是)只会添加该类型的见证人并继续前进,但是您... (2认同)