当前位置: 首页 > 面试题库 >

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

慕容兴贤
2023-03-14
问题内容

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

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调用链,可以使用目标类型的最后一个方法调用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 factory进行编译时不会出现问题:

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正确理解参数?

  • 我一直在研究集合之间的差异。排序和列表。排序,特别是关于使用比较器静态方法以及lambda表达式中是否需要参数类型。在我们开始之前,我知道我可以使用方法引用,例如Song::getTitle来克服我的问题,但我这里的查询并不是我想要修复的东西,而是我想要的答案,即为什么Java编译器会以这种方式处理它。 这些是我的发现。假设我们有一个类型的,添加了一些歌曲,有3个标准get方法: 以下是对两种类型

  • 我有一个关于compareTo函数如何帮助比较器排序的问题,即o1。比较(o2)与o2。比较(o1) 如果两个字符串相等,则此方法返回0,否则返回正值或负值。如果第一个字符串在词典上大于第二个字符串,则结果为正,否则结果为负。 上面的陈述很简单,但是为什么o1.compare(o2)会给我一个升序,而o2.compare(o1)给了我一个降序? 如果我有整数值“5,10,3”,我得到3,5,10和

  • 问题内容: 比较器内部的返回值实际上是什么意思? 例如 : 如果返回类型为1,则其实际返回 [20、10、30、100] 如果返回类型为-1,则其实际返回 [100,30,10,20] 如果返回类型为0,则其实际返回 [20] 请告诉我这表示什么? 问题答案: 返回值(不是类型是)告诉调用者(对数据进行排序的事物): 如果始终为比较器返回相同的值(o,1,-1),而不管其输入如何,那么您使用的是错

  • 问题内容: 我需要编写一个比较器,它采用类型A的对象A和类型B的对象B。这两个对象不是公共对象的扩展。它们的确不同,但是我需要通过其中的通用字段来比较这两个对象。我必须使用比较器接口,因为对象存储在Set中,并且在必须对CollectionUtils执行操作之后。我在Google上搜索了一下,发现了Comparator的解决方案,但只有相同的类型。 我试图朝这个方向实施思考,但是我不知道我是否在正

  • 我需要写一个比较器,取一个a类型的对象a和一个B类型的对象B。这两个对象不是一个公共对象的扩展。他们确实是不同的,但我需要比较这两个对象在它的共同领域。我必须使用比较器接口,因为对象存储在Set中,之后我必须使用CollectionUtils进行操作。我搜索了一点点,我用比较器找到了解决方案,但只有相同的类型。 TXS 附注:我在不同的集合中添加两个对象: 之后我会这样想: