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

时间:2018-11-27 08:34:09

标签: java generics java-stream language-lawyer type-inference

我有以下简化的示例,以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);
}

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

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

() -> new TreeMap<Integer, List<String>>(Comparator.reverseOrder()), // Type required

所以:

  • 编译器能够推断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)

2 个答案:

答案 0 :(得分:6)

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

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

首先,表达式的目标类型不用于接收方表达式,即方法调用链中。因此,当您使用以下形式的声明

TargetType x = first.second(…).third(…);

TargetType将用于推断third()调用及其参数表达式的通用类型,但不适用于second(…)调用。因此,second(…)的类型推断只能使用first的独立类型和参数表达式。

这不是问题。由于list的独立类型已很好地定义为List<String>,因此推断Stream<String>调用和有问题的{{1 }}调用是链的最后一个方法调用,它可以使用目标类型stream()来推断类型参数。

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

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


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

来解决
groupingBy

这为分组函数使用了显式键入的lambda表达式,虽然实际上没有贡献地图键的类型,但使编译器找到了实际的类型。

尽管使用显式类型的lambda表达式而不是隐式类型的表达式可以在方法重载分辨率方面有所作为,如上所述,但不应在此处应用,因为此特定情况不是重载方法的问题。

奇怪的是,即使进行以下更改也会使编译器错误消失:

groupingBy

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

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

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

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

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()
    ));

而使用显式类型的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);
}

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

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

答案 1 :(得分:0)

Java类型推断的功能有很多限制。在很多情况下,人类读者可以说出类型应该是什么,但是编译器无法推断出它。一个简单的案例(展示了与您的示例类似的局限性):

private static <K,T> T test(K key, Supplier<Map<K,T>> arg) {
    return arg.get().get(key);
}

test("Foo", () -> new TreeMap<>(Comparator.reverseOrder()));

人类读者可以看到比较器比较字符串,因为第一个参数的类型也是映射的键,但是Java无法推断出它。就像您的情况一样,添加类型见证人 Comparator.<String>可以解决歧义。

这是Java类型系统和将泛型添加到语言中的自然结果。如果您有兴趣阅读详细信息,请参阅Java Language Specification