为什么不能传递<! - ? - >或<! - ?将T - >泛型类实例扩展为期望<! - ?的方法超级T - >?

时间:2016-07-30 21:56:45

标签: java generics extends super

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

让我们考虑以下计划:

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规则的对称性,但我没有找到任何对称性。 所以:

  • 为什么无法将<?><? extends T>传递给期待<? super T>的函数?
  • 为什么可以将<?>传递给期望<? extends T>的函数?
  • 为什么可以将<? super T>传递给期待<? extends T>的函数?

4 个答案:

答案 0 :(得分:2)

您可以将泛型方法(即具有自己的类型参数的方法)视为关于其类型参数的一组语句。

例如,方法 <T> someMethod(List<T>) 说:

  

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

易。所有列表都符合此条件,因此任何列表都是该方法的有效输入。

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

方法声明 <T> someMethod(List<? extends T>) 声明略有不同:

  

存在某种类型T,此列表的类型参数是T的子类型。

对于所有列表,这也很简单。但是,您可能会注意到它使<T> someMethod(List<? extends T>)有点无用。您告诉编译器捕获以下事实:列表中的项目与列表中的其他项目共享一些常见的超类型。除非你在方法中有一些已知接受<? super T>的其他消费者,否则你无法对这些信息做任何事情。它比来自列表的所有项目属于同一类型的知识要少得多。

那么,<T> takeSuper(List<? super T>)为什么不以同样的方式工作?

方法声明 <T> takeSuper(List<? super T>) 可以解释为声明:

  

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

如果我们有List<? super Long>并将其传递给捕获<T> takeSuper(List<? super T>)的方法,则很容易看到Long类型的引用会满足T。我们可以将Long作为T类型的参数传递给方法,然后将其从方法内部添加到列表中。

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

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

有趣的是,我的编译器(Java 8)并未抱怨使用<T> takeSuper(List<? super T>)作为输入调用方法List<?>。我想它认识到,由于无法获得类型T的引用,因此忽略无用的类型参数没有任何害处。

答案 1 :(得分:1)

  

为什么无法将<?><? extends T>传递给期待<? super T>的函数?

考虑这种方法:

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<Integer>

  

为什么可以将<?>传递给期望<? extends T>的函数?

takeExtend(unbound);

T没有界限;它可以是任何类型。虽然unbound的类型参数是 unknown ,但它确实有某些类型,并且由于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));
}
  

为什么可以将<? super T>传递给期望<? extends T>的函数?

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

static <T> void takeExtend(List<? extends T> l)

List<? super Long> superBound = new ArrayList<Long>();
takeExtend(superBound);

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

T并未推断为Long。如果您明确指定Long作为类型参数,您将看到它不起作用:

Main.<Long>takeExtend(superBound);

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

答案 2 :(得分:1)

  • 为什么无法将<?><? extends S>传递给期待<? super T>的函数? (为清楚起见,问题已得到纠正:?扩展 S )。

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

  • 为什么可以将<?>传递给期望<? extends T>的函数?

它是关于推理的。您可以传递<?>,因为编译器可以从中创建一些东西。也就是说,它可以使对象脱离T.以下情况也是如此:

List<?> unbound = new ArrayList<>();
  • 为什么可以将<? super T>传递给期待<? extends T>的函数?

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

答案 3 :(得分:0)

我一直在想这些东西,在你的评论的惊人帮助下,试图找到一个易于记忆的解决方案。

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

  • 如果我们传递<? extends T>,则已经定义了上限和它 将使用那个
  • 如果我们通过<?>,我们什么都不知道,但我们知道 每个类都扩展了Object,因此编译器会推断出Object
  • 如果我们通过<? super T>,我们知道下限,但是因为每个班级 extends Object,编译器推断Object

接受<? super T>以获取T的方法必须推断该层次结构的下限。在Java中,默认情况下我们可以指望上限,但不能指向下限。因此,前一种情况下的考虑因素不再有效。所以:

  • 如果我们传递<? extends T>,则没有下限,因为 层次结构可以根据需要进行扩展
  • 如果我们通过<?>, 没有下限,因为层次结构可以扩展为 就像我们想要的那样
  • 如果我们传递<? super T>,则下限已经存在 定义,所以我们可以使用那个。

重新盖上:

  • 接受<? extends T>的方法总是可以依赖于这样的事实 存在上限,即Object - &gt;我们可以传递每一个界限
  • 接受<? super T>的方法只有在参数中明确定义的情况下才能推断出下限 - &gt;我们只能传递<? super T>