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

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

我有以下简化示例,它以从整数到列表的 TreeMap 形式将字符串列表分组到类别中

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)

最佳答案

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

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

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

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

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

这不是问题。自单机型list明确定义为 List<String> ,推断结果类型没有问题 Stream<String>对于 stream()打电话和有问题的collect call是链的最后一个方法调用,可以使用目标类型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()
    ));

这对分组函数使用了一个显式类型化的 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()
    ));

在这里,我们没有帮助任何额外的显式类型,也没有改变 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);
}

然后,使用隐式类型的 lambda 表达式作为 map 工厂编译没有问题:

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

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

在我看来,即使规范支持这种行为,它也应该得到纠正,因为提供显式类型的含义永远不应该是类型推断比不提供更糟糕。对于零参数 lambda 表达式尤其如此,我们无法将其转换为隐式类型表达式。

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

关于java - 流中收集器中的比较器导致类型推断问题?,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/56165072/

相关文章:

ios - 非标称类型 'User' 不支持显式初始化

generics - 在 Swift 中,如何将函数一般限制为理解 T + T 的类型

java - 使用具有多个字段的不同类型列表的流来断言等于

java - 为什么 Stream.flatMap 不能接受一个集合?

java - 将流拆分为具有 N 个元素的子流

java - 将正整数范围映射到索引范围

java - 无法使用 JDBC 驱动程序连接到 Salesforce

java - 安卓 : Why occur "NotSerializableException" even if implements "Serialization"?

java - 无法使用 jaxb 将 XML 持久保存到数据库

c# - 使用属性对两个复杂类型的泛型集合进行排序