15

我遇到了以下代码:

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>,所以推断的类型varTreeSet<? super T>

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

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

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

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


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

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

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


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

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>set局部变量的类型?

4

2 回答 2

9

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

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

关于您问题中的代码,唯一可推断的类型(通过使用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:局部变量声明符和类型似乎支持我的主张,陈述如下:

在此处输入图像描述

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

list我相信这与invar list = List.<Number>of(1, 2, 3);是 aList<Number>而不是 a的原因相同List<Integer>,没有var.

于 2018-04-02T18:56:49.873 回答
3

在第二个代码段中使用局部变量需要您明确指定TreeSetas in 的边界:

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

简单地问一下,默认情况下,您希望在这里推断出什么类型TreeSetIntegerByte子类列表中的哪一个Number以及如何(基于可能稍后推断的返回类型)?

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

于 2018-04-02T19:27:42.170 回答