泛型类型变量中的局部类型推断和逆变

Local type inference and contravariance in generic type variables

我遇到了以下代码:

public static <T> Set<T> distinct(
        Collection<? extends T> list,
        Comparator<? super T> comparator) {

    Set<T> set = new TreeSet<>(comparator);
    set.addAll(list);
    return set;
}

此代码仅使用一个中间体 TreeSet 来删除重复项,其中元素之间的相等性是根据提供的比较器定义的。

让我们给本地类型推断一个机会,我(天真地)想...所以我将上面的代码改为:

public static <T> Set<T> distinct(
        Collection<? extends T> list,
        Comparator<? super T> comparator) {

    var set = new TreeSet<>(comparator);
    set.addAll(list);
    return set;
}

这对我来说很有意义,因为 set 的类型可以从 comparator 的类型推断出来,我是这么认为的。但是,修改后的代码无法编译并生成以下错误:

java: incompatible types: java.util.TreeSet<capture#1 of ? super T> cannot be converted to java.util.Set<T>

现在明白为什么会报错了,我承认比较器的类型其实是Comparator<? super T>,所以var推断出的类型是TreeSet<? super T>

但是,我想知道 为什么 var 无法将 TreeSet 的通用类型推断为 T 而不是 ? super T.毕竟,according to the docs,一个TreeSet<E>有一个接受类型Comparator<? super E>参数的构造函数。所以调用这个构造函数应该创建一个 TreeSet<E>,而不是 TreeSet<? super E>。 (这是第一个片段显示的内容)。我希望 var 遵循同样的逻辑。

注 1: 编译代码的一种方法是将 return 类型更改为 Set<? super T>。但是,那将是一个几乎无法使用的集合...

注释 2: 另一种方法是不在比较器中使用逆变,但我不想这样,因为我无法使用 Comparator 比较 T.

的祖先

注意 3: 我知道第一个片段有效,所以很明显我应该坚持不使用 var 并将集合明确声明为 Set<T>。但是,我的问题不是我是否应该丢弃我的第二个片段或如何修复它。相反,我想知道为什么 var 没有将 TreeSet<T> 推断为我的第二个代码段中 set 局部变量的类型。


编辑 1:this comment 中,用户 @nullpointer 正确指出我应该进行以下细微更改以使第二个代码段编译:

var set = new TreeSet<T>(comparator); // T brings in the magic!

现在泛型类型参数 T 对于 TreeSet 是显式的,因此 var 正确地将 set 局部变量的类型推断为 TreeSet<T>。不过,我想知道为什么我必须明确指定 T


编辑 2:this other comment 中,用户 @Holger 巧妙地提到以下语言是 forbidden

var set = new TreeSet<? super T>(comparator);

以上代码编译失败,出现以下错误:

java: unexpected type
  required: class or interface without bounds
  found:    ? super T

所以现在问题变得更加明显:如果我不能在实例化表达式 new TreeSet<? super T>(comparator) 中显式指定有界泛型类型 ? super T,为什么编译器将 TreeSet<? super T> 推断为类型set 局部变量的?

根据 on ,他说:

Local variable type inference says: the types I need are probably already present on the right hand side, why repeat them on the left.

关于您问题中的代码,唯一可推断的类型(通过使用提供的 Comparator)是 TreeSet<? super T>。我们人类足够聪明,可以看到 distinct returns set 并期待 Set<T>。然而,编译器可能不够聪明,无法解决它(我相信它可以),但更有可能的是 var 使用 RHS 上提供的信息推断出最具体的类型,而架构师不想打破它。

现在,正如 nullpointer 在他们的评论中所述,您可以将 TreeSet 明确定义为 T 类型,而不是推断的捕获类型 ? super T,如下所示:

var set = new TreeSet<T>(comparator);

我假设显式泛型类型会覆盖传递给构造函数的推断类型,这是有道理的。

JLS §14.4.1: Local Variable Declarators and Types 似乎支持我的说法,声明如下:

注意:"upward projection of T",它可能只是推断类型(TreeSet 而不是 Set),但可能包括泛型也输入。

我认为这与 var list = List.<Number>of(1, 2, 3); 中的 listList<Number> 而不是 List<Integer> 的原因相同,没有 var 也一样有效.

在第二个代码段中使用局部变量需要您明确指定 TreeSet 的范围,如 :

public static <T> Set<T> distinct(Collection<? extends T> list, Comparator<? super T> comparator) {
    var set = new TreeSet<T>(comparator);
    set.addAll(list);
    return set;
}

原因是推断的 var 否则会使用与比较器一起使用的最明显的界限,并被推断为 TreeSet<? super T> 并且由于转换不兼容而导致声明的编译错误失败。

why I must specify T explicitly

正如雅各布所指出的那样反过来思考并将代码制定为

  private static Set<Integer> distincts(Collection<? extends Integer> list, Comparator<Number> comparator) {
        var set = new TreeSet<>(comparator);
        // if you don't specify the bound, you get a compiler error on the return statement 
        // since the inferred type would be `Number`
        set.addAll(list);
        return set;
    }

简单地反问一下,对于TreeSet,您希望此处默认推断出什么类型? IntegerByte 哪一个来自 Number 的子类列表以及如何(基于 return 类型,这可能会在很晚的时候推断出来)?

Edit:- 我确实认为给定构造函数 TreeSet(Comparator<? super E> comparator) constructs a TreeSet<E>, hence invocation of such constructor should be inferred as TreeSet<E> instead of TreeSet<? super E>. Also, as , not everything could be inferred and for any such specific types, one could ask for it(assuming on the jdk mailing list).