【问题标题】:Why is it not possible to pass a <?> or a <? extends T> generic class instance to a method expecting a <? super T>?为什么不能传递 <?> 或 <?将 T> 泛型类实例扩展为期望 <?超级T>?
【发布时间】:2016-07-30 21:56:45
【问题描述】:

PECS 原则是关于您在函数中选择哪种类型的参数,具体取决于您将如何使用该参数。 我的问题是,一旦您选择使用super(因为您的函数可能是消费者),您就不能将某些泛型类实例传递给该函数。

让我们考虑以下程序:

public class Main{
    public static void main(String[] args){
        List<?> unbound = new ArrayList<Long>();
        List<? extends Long> extendsBound = new ArrayList<Long>();
        List<? super Long> superBound = new ArrayList<Long>();

        takeExtend(unbound);
        takeExtend(extendsBound);
        takeExtend(superBound);

        takeSuper(unbound);
        takeSuper(extendsBound);
        takeSuper(superBound);
    }

    static <T> void takeExtend(List<? extends T> l){}
    static <T> void takeSuper(List<? super T> l){}
}

编译器给出以下错误:

error: method takeSuper in class Main cannot be applied to given
types;
        takeSuper(unbound);
        ^   required: List<? super T>   found: List<CAP#1>   reason: cannot infer type-variable(s) T
    (argument mismatch; List<CAP#1> cannot be converted to List<? super T>)   where T is a type-variable:
    T extends Object declared in method <T>takeSuper(List<? super T>)   where CAP#1 is a fresh type-variable:
    CAP#1 extends Object from capture of ?

我试图找到一种对称性,例如我们为 PECS 规则提供的对称性,但我没有找到。 所以:

  • 为什么不能将&lt;?&gt;&lt;? extends T&gt; 传递给期望&lt;? super T&gt; 的函数?
  • 为什么可以将&lt;?&gt; 传递给需要&lt;? extends T&gt; 的函数?
  • 为什么可以将 &lt;? super T&gt; 传递给需要 &lt;? extends T&gt; 的函数?

【问题讨论】:

  • 我已经掌握了PECS原理,但这个问题是关于他在不同种类的仿制药中的应用。
  • 抱歉,在评论之前我没有仔细阅读您的问题。我删除了我的评论;我同意这不一样。
  • 你的 Java 版本是多少?这适用于 Java 8 及更高版本。
  • 我使用的是 Java 1.8.0_60,但它不适合我。
  • 我有点认为这个编译器错误是一个错误。它应该捕获转换然后推断TCAP#1List&lt;CAP#1&gt;List&lt;? super CAP#1&gt; 所以应该没问题。这是通过在单独的步骤中进行捕获转换的一种解决方法:ideone.com/DBefuU

标签: java generics extends super


【解决方案1】:

您可以将泛型方法(即具有自己的类型参数的方法)视为对其类型参数进行一组声明。

例如,方法 &lt;T&gt; someMethod(List&lt;T&gt;) 表示:

存在一些类型T,该列表的类型参数等于T

简单。所有列表都符合此标准,因此任何列表都是该方法的有效输入。

方法中的所有内容现在都可以利用该陈述为真的知识。这些知识有什么用?好吧,如果我们从列表中获取一个项目,我们就知道它与列表的类型参数匹配。因为我们将类型捕获为T,所以我们可以持有对该对象的引用并稍后将其放回列表中,但仍然不知道它到底是什么类型。

方法声明 &lt;T&gt; someMethod(List&lt;? extends T&gt;) 做了一个稍微不同的声明:

存在一些类型T,该列表的类型参数是T的子类型。

这对于所有列表也是如此。但是,您可能会注意到它使&lt;T&gt; someMethod(List&lt;? extends T&gt;) 有点无用。您告诉编译器捕获列表中的项目与列表中的其他项目共享一些公共超类型的事实。除非您在方法中拥有其他已知接受&lt;? super T&gt; 的消费者,否则您无法处理该信息。它远不如知道来自列表的所有项目都属于同一类型。


那么,为什么&lt;T&gt; takeSuper(List&lt;? super T&gt;) 的工作方式不同?

方法声明&lt;T&gt; takeSuper(List&lt;? super T&gt;)可以解释为声称:

存在一些类型T,该列表的类型参数是T的超类型。

如果我们有一个List&lt;? super Long&gt; 并将它传递给一个捕获&lt;T&gt; takeSuper(List&lt;? super T&gt;) 的方法,那么很容易看出Long 类型的引用将满足T。我们可以将Long 作为T 类型的参数传递给方法,然后从方法内部将其添加到列表中。

但是如果我们有一个List&lt;?&gt; 并且我们使用&lt;T&gt; takeSuper(List&lt;? super T&gt;) 方法捕获它的类型呢?通过将列表声明为List&lt;?&gt;,我们说我们目前不知道它的类型参数是什么。在这样做时,我们告诉编译器我们绝对无法获得与列表的类型参数匹配的类型的引用。换句话说,编译器确定没有对象可以满足类型参数T。请记住,对于此方法,如果 已知 对象的类型是列表的类型参数的子类型,则该对象的类型为 T。如果我们对列表的类型参数一无所知,那是不可能的。

List&lt;? extends Long&gt; 也是如此。我们知道我们从列表中获取的项目将是Long 的子类型,但我们没有为它们的类型设置下限。我们永远无法证明任何类型都是列表类型参数的子类型。因此,对于&lt;T&gt; takeSuper(List&lt;? super T&gt;) 方法,再次证明无法获得T 类型的引用。

有趣的是,我的编译器 (Java 8) 并没有抱怨使用 List&lt;?&gt; 作为输入来调用方法 &lt;T&gt; takeSuper(List&lt;? super T&gt;)。我想它认识到,由于无法获得T 类型的引用,因此忽略无用的类型参数并没有什么坏处。

【讨论】:

    【解决方案2】:

    为什么不能将&lt;?&gt;&lt;? extends T&gt; 传递给期望&lt;? super T&gt; 的函数?

    考虑这种方法:

    static <T> void sillyAdd(List<? super T> l, T t){
      l.add(t);
    }
    

    现在看看如果可能的话我们可以如何使用它:

    List<Integer> onlyIntegers = new ArrayList<>();
    List<? extends Number> anyNumber = onlyIntegers;
    sillyAdd(anyNumber, Float.valueOf(0)); /* Not allowed... */
    Integer i = onlyIntegers.get(0);
    

    最后一行会抛出一个ClassCastException,因为我们可以将Float 放入List&lt;Integer&gt;

    为什么可以将&lt;?&gt; 传递给期望&lt;? extends T&gt; 的函数?

    takeExtend(unbound);
    

    T 没有界限;它可以是任何类型。虽然unbound 的类型参数是unknown,但它确实有some 类型,并且由于T 可以是任何类型,所以它是匹配的。

    像这样的方法有时会在泛型的某些奇怪角落用作帮助器。逻辑上,我们知道以下应该没问题:

    public static void rotate(List<?> l) {
      l.add(l.remove(0));
    }
    

    但是泛型的规则在这里并不意味着类型安全。相反,我们可以使用这样的帮助器:

    public static void rotate(List<?> l) {
      helper(l);
    }
    
    private static <T> void helper(List<T> l) {
      l.add(l.remove(0));
    }
    

    为什么可以将&lt;? super T&gt; 传递给期望&lt;? extends T&gt; 的函数?

    在某种程度上,它不是。给定这个例子:

    static <T> void takeExtend(List<? extends T> l)
    
    List<? super Long> superBound = new ArrayList<Long>();
    takeExtend(superBound);
    

    您可能认为T 被推断为Long。这意味着您将List&lt;? super Long&gt; 传递给声明为void takeExtend(List&lt;? extends Long&gt; l) 的方法,这似乎是错误的。

    T 不会被推断为Long。如果你明确指定 Long 作为类型参数,你会发现它不起作用:

    Main.<Long>takeExtend(superBound);
    

    实际发生的情况是T 被推断为? super Long,因此该方法的泛型类型类似于void takeExtend(List&lt;? extends ? super Long&gt;)。这意味着列表中的所有内容都扩展了未知的超类Long

    【讨论】:

    • 关于第一个答案。我完全同意这一点,但这样你就依赖于我们将使用 super T> 用于在其中添加元素的参数,如果添加是在函数外部执行的,这也是有效的。因此,不允许的是添加,而不是将参数传递给函数,如果我们从函数中删除添加语句,则允许这样做。关于第二个答案。如果 T 可以是任何类型,为什么在函数接受 super 时它也无效?
    • @acejazz "所以,不允许的是添加,而不是向函数传递参数,如果我们从函数中删除添加语句,则允许这样做。"这不符合语法,所以我不确定我理解你在说什么。编译器不允许将该类型的引用作为该类型的参数传递(在这种情况下也不允许赋值)。 它与方法内部发生的事情无关;编译器不考虑这一点。但是编译器不允许它是有原因的,而不仅仅是为了好玩。我的例子说明了原因。
    • 抱歉我的语法不好。正如我所知道的,使用 super T> 作为函数中的参数是用于放入对象(Consumer-Super),所以因为你不能用 扩展为编译器不允许的实际参数。我说的对吗?
    【解决方案3】:
    • 为什么不能将&lt;?&gt;&lt;? extends S&gt; 传递给期望&lt;? super T&gt; 的函数? (为清楚起见更正了问题:? 扩展 S)。

    编译器想要推断出 T,它是下限,但它不能。每个类都扩展了 Object,但没有通用的继承树底层类,编译器可以默认使用它。

    • 为什么可以将&lt;?&gt; 传递给需要&lt;? extends T&gt; 的函数?

    这是关于推理的。您可以传递&lt;?&gt;,因为编译器可以从中做出一些事情。也就是说,它可以将Object从T中取出来。下面的情况也是如此:

    List<?> unbound = new ArrayList<>();
    
    • 为什么可以将 &lt;? super T&gt; 传递给需要 &lt;? extends T&gt; 的函数?

    再次,现在是关于推断上限,因此编译器可以默认为 Object。它对什么不感兴趣?对任何东西都超级,也不是对任何东西都超级。

    【讨论】:

    • 与其说是推理,不如说是捕捉。编译器并不关心 T 实际上是什么类型。它只关心标记为 T 的所有引用都属于 same 类型。擦除后,反正一直是Objects。
    • 但是如果你不仅有 T,还有 S、Q、R、V、W 等,它们都在一个更大的层次结构中占有一席之地,并且有很多相互关系,那么就有一些推断,不是吗?
    • 当然。我只是在质疑“[编译器]可以从 T 中生成对象”的想法。我认为这是一个误导性的想法。泛型方法签名实际上是对其类型参数进行一组断言。当且仅当该组断言有一个有效的解决方案时,类型推断才是可能的。实际类型并不真正相关,只有它们之间的关系。
    • 老实说,我不知道这是否具有误导性。但它非常简单地说明了逻辑。 @erickson 在回答的最后给出了他的版本。我想询问消息来源或证明但没有足够的声誉。
    【解决方案4】:

    在你们 cmets 的惊人帮助下,我整晚都在想这些东西,试图找到一个容易记住的解决方案。

    这就是我得到的。 接受&lt;? extends T&gt; 的方法要获得T,必须推断出该层次结构的上限。由于在 Java 中每个类都在扩展 Object,因此编译器可以依赖于这样一个事实,即至少上限是 Object。所以:

    • 如果我们传递一个&lt;? extends T&gt;,则上限已经定义并且它 将使用那个
    • 如果我们通过&lt;?&gt;,我们什么都不知道,但我们知道 每个类都扩展了 Object,所以编译器会推断出Object
    • 如果我们通过&lt;? super T&gt;,我们知道下限,但是由于每个类 extends Object,编译器推断Object

    接受&lt;? super T&gt; 的方法要获得T,必须推断出该层次结构的下限。默认情况下,在 Java 中,我们可以依靠上限,但不能依靠下限。因此,在前一个案例中所做的考虑不再有效。所以:

    • 如果我们通过&lt;? extends T&gt;,则没有下限,因为 层次结构可以随心所欲地扩展
    • 如果我们通过&lt;?&gt;, 没有下界,因为层次结构可以扩展为 随心所欲
    • 如果我们通过&lt;? super T&gt;,下限已经是 定义,所以我们可以使用那个。

    重述:

    • 接受&lt;? extends T&gt; 的方法始终可以依赖于以下事实: 存在一个上限,即 Object -> 我们可以将每个边界传递给它
    • 接受&lt;? super T&gt; 的方法只有在参数中明确定义时才能推断出下限 -> 我们只能传递&lt;? super T&gt;

    【讨论】:

    • 是和不是。请记住,编译器在任何时候都不关心 T 实际上是什么类型。它不是“对象”(或任何其他类型,就此而言)。只是T。
    • 我明白你的意思。编译器不会推断 Object,但在这种情况下,编译器会设法将 T 替换为 Object。我该如何改写我的答案,将其从“是和否”变为“是”? :)
    猜你喜欢
    • 2010-11-01
    • 1970-01-01
    • 1970-01-01
    • 2012-08-30
    • 1970-01-01
    • 2011-05-26
    • 1970-01-01
    相关资源
    最近更新 更多