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

我遇到了以下代码:

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;
}

这对我来说是有道理的,因为的类型可以从的类型中推断出来,或者我是这么认为的。但是,修改后的代码不会编译并生成以下错误:setcomparator

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

现在,我明白了为什么会发生错误,我承认比较器的类型实际上是,所以推断的类型是。Comparator<? super T>varTreeSet<? super T>

但是,我想知道为什么var不能将TreeSet的泛型类型推断为T而不是?超级T。毕竟,根据文档,a 有一个构造函数,该构造函数接受类型的参数。因此,调用此构造函数应创建一个 ,而不是一个 。(这是第一个代码段显示的内容)。我希望遵循同样的逻辑。TreeSet<E>Comparator<? super E>TreeSet<E>TreeSet<? super E>var

注1:进行代码编译的一种方法是将返回类型更改为 。但是,这将是一个几乎不可用的集合...Set<? super T>

注2:另一种方法是不要在比较器中使用逆变,但我不想要这个,因为我无法使用比较 祖先的。ComparatorT

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


编辑1:在此评论中,用户@nullpointer正确地指出,我应该进行以下细微的更改以进行编译第二个代码段:

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

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


编辑2:在另一条评论中,用户@Holger巧妙地提到该语言禁止以下内容

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

上面的代码无法编译,并出现以下错误:

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

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


答案 1

根据Brian Goetz对我问题的回答,他说:

局部变量类型推断说:我需要的类型可能已经出现在右侧,为什么要在左侧重复它们。

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

现在,正如 nullpointer 在其注释中所述,您可以使用以下内容显式定义类型,而不是推断的捕获类型:TreeSetT? super T

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

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

JLS §14.4.1:局部变量声明符和类型似乎支持我的主张,并指出以下内容:

enter image description here

注意:“T 的向上投影”,它可能只是推断类型(而不是 ),但也可能包括泛型类型。TreeSetSet

我相信这与为什么 in 是 a 而不是 a 的原因相同,后者在没有 .listvar list = List.<Number>of(1, 2, 3);List<Number>List<Integer>var


答案 2

在第二个代码段中使用局部变量需要显式指定 的边界,如 中所示: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>

为什么我必须显式指定 T

正如雅各布所指出的,反之亦然,并将代码表述为

  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;
    }

简单地问一下,默认情况下,您希望在这里推断出哪种类型?,从子类列表中的哪一个以及如何(基于返回类型,可能只是在很久以后推断出来)?TreeSetIntegerByteNumber

编辑:- 我再次认为给定构造函数 TreeSet(比较器<? 超级 E> 比较器)构造一个 ,因此应该推断出调用这样的构造函数而不是 .此外,正如Brian所评论的那样,并非所有内容都可以推断出来,对于任何此类特定类型,都可以要求它(假设在jdk邮件列表中)。TreeSet<E>TreeSet<E>TreeSet<? super E>


推荐