【问题标题】:Local type inference and contravariance in generic type variables泛型类型变量中的局部类型推断和逆变
【发布时间】:2018-04-02 18:11: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&lt;? super T&gt;,所以var 推断的类型是TreeSet&lt;? super T&gt;

但是,我想知道为什么var 不能将TreeSet 的泛型类型推断为T 而不是? super T。毕竟,according to the docsTreeSet&lt;E&gt; 有一个接受 Comparator&lt;? super E&gt; 类型参数的构造函数。所以调用这个构造函数应该创建一个TreeSet&lt;E&gt;,而不是TreeSet&lt;? super E&gt;。 (这是第一个 sn-p 显示的内容)。我希望var 遵循同样的逻辑。

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

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

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


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

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

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


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

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

上面的代码编译失败,出现如下错误:

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

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

【问题讨论】:

  • 评论不用于扩展讨论;这个对话是moved to chat
  • 我认为是因为涉及到两个以上的人,所以没有出现自动链接,mods必须手动执行操作。如果你真的想在这种场景下聊天,可以参考这个meta.stackexchange.com/questions/106467
  • 好吧,我猜这个问题的 cmets 已经被版主或用户标记了。您可能想更新问题,链接已损坏。

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


【解决方案1】:

根据Brian Goetz' answer 上的my question,他说:

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

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

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

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

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

JLS §14.4.1: Local Variable Declarators and Types 似乎支持我的主张,陈述如下:

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

我相信这与为什么var list = List.&lt;Number&gt;of(1, 2, 3); 中的listList&lt;Number&gt; 而不是List&lt;Integer&gt; 的原因相同,没有var 也一样。

【讨论】:

  • 您好,Jacob,感谢您的出色回答。是的,我同意本地类型推断需要的类型在 RHS 上,并且在我的示例中,RHS 上的泛型类型实际上是 ? super T。但是,我想知道为什么var 机械地推断TreeSet&lt;? super T&gt;。谁想要局部变量中的有界泛型类型?有界泛型类型参数在方法参数中使用时最有用。 var 不能足够聪明地推断出T 吗?我想这是我问题的核心......
  • 我很确定类型推断不会搜索当前语句之外的提示。如果语句是return,它可以查询当前方法的签名,如果该语句包含方法调用,它可以探测被调用方法的签名。但是,它不会展望分配目标的未来用途。它不会检查返回值,因为 return 语句出现在当前语句之后。因此,var set 的类型将在 参考声明方法的签名的情况下推断出来。
  • 我相信它可以足够智能,但是,正如 Mike 刚才所说,它没有理由向前看,因为它只使用了 RHS 的信息同一行来推断类型。如果这是未来的修改,我绝对会欢迎!
  • @MikeStrobel 是的,我知道类型推断不会向前看,我并不是说在这种情况下它应该这样做。我了解var 旨在仅在 RHS 中查找类型,这没关系。我想知道为什么set 的类型被推断为TreeSet&lt;? super T&gt; 而不是TreeSet&lt;T&gt;。我的观点是:在这种情况下不能改进类型推断吗?我认为使用有界泛型类型的局部变量没有意义,特别是如果泛型类型由类型变量而不是实际类型表示。
  • @FedericoPeraltaSchaffner 每次我们开发人员发现这样的案例时,都会想为什么不在这里呢?我已经多次听到这作为一个论点,我想这本来可以做到的,但它要么不像看起来那么简单,要么会带来更复杂的代码,因为这种情况并不常见。我猜……
【解决方案2】:

在第二个 sn-p 中使用局部变量需要您明确指定 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&lt;? super T&gt;,并且由于转换不兼容而导致编译错误而失败。

为什么我必须明确指定 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 推断出什么类型? IntegerByteNumber 的子类列表中的哪一个以及如何(基于可能稍后推断的返回类型)?

编辑:- 我确实认为构造函数 TreeSet(Comparator&lt;? super E&gt; comparator) 构造了一个TreeSet&lt;E&gt;,因此调用了这样的构造函数应该推断为TreeSet&lt;E&gt; 而不是TreeSet&lt;? super E&gt;。此外,作为Brian commented,并非所有内容都可以推断出来,对于任何此类特定类型,都可以要求它(假设jdk mailing list)。

【讨论】:

  • 感谢您的回答。我认为比较器使用的界限是由var 机械推断的界限并不那么明显,这就是我的问题的重点。关于答案末尾旁边的示例,我认为这与我的问题不同。您应该收到一个Comparator&lt;? super Integer&gt; 以使其类似,然后您可以使用一些Comparator&lt;Number&gt;Comparator&lt;Integer&gt; 调用该方法。现在你会得到一个模仿我的不兼容类型编译错误,虽然使用特定类型Integer 而不是只有T
猜你喜欢
  • 1970-01-01
  • 2013-11-29
  • 2021-08-14
  • 2018-09-09
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2023-03-06
  • 1970-01-01
相关资源
最近更新 更多