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

时间:2018-04-02 18:11:15

标签: java generics type-inference contravariance java-10

我遇到了以下代码:

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 docsTreeSet<E>有一个接受Comparator<? super E>类型参数的构造函数。因此,调用此构造函数应创建TreeSet<E>,而不是TreeSet<? super E>。 (这是第一个片段显示的内容)。我希望var遵循同样的逻辑。

注1:编译代码的一种方法是将返回类型更改为Set<? super T>。但是,这将是一个难以使用的设置......

注2:另一种方法是不在比较器中使用逆变,但我不想这样,因为我不能使用{{1比较Comparator的祖先。

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

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

set

现在,var set = new TreeSet<T>(comparator); // T brings in the magic! 明确了泛型类型参数T,因此TreeSet正确地将var局部变量的类型推断为set。不过,我想知道为什么我必须明确指定TreeSet<T>

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

T

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

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

所以现在问题变得更加明显:如果我无法在实例化表达式java: unexpected type required: class or interface without bounds found: ? super T 中明确指定有界泛型类型? super T,为什么编译器会将new TreeSet<? super T>(comparator)推断为类型TreeSet<? super T>局部变量?

2 个答案:

答案 0 :(得分:8)

根据Brian Goetz' answermy question,他说:

  

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

关于问题中的代码,唯一可用于推断的类型(使用提供的Comparator)是TreeSet<? super T>。我们人类足够聪明,可以看到distinct返回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似乎支持我的主张,说明如下:

enter image description here

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

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

答案 1 :(得分:3)

在第二个代码段中使用局部变量要求您明确指定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

<击> 按照Jacob所指出的另一种方式思考并将代码表示为

  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的子类列表中的哪一个以及如何(基于可能稍后推断的返回类型)?

编辑 : - 我认为给定构造函数 TreeSet(Comparator<? super E> comparator) 构建TreeSet<E>,因此应该将此类构造函数的调用推断为TreeSet<E>而不是TreeSet<? super E>。此外,作为Brian commented,并非所有内容都可以被推断,对于任何此类特定类型,可以要求它(假设在jdk mailing list上)。