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



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>的函数

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

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

存在某种类型的CCD_ 10,该列表的类型参数等于CCD_。

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

该方法中的所有内容现在都可以利用该语句为真的知识。这些知识怎么有用?如果我们从列表中得到一个项,我们就知道它与列表的类型参数匹配。因为我们将类型捕获为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>)可以被解释为声称:

存在某种类型的CCD_ 20,该列表的类型参数是CCD_。

如果我们有一个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的一个子类型,但我们没有它们类型的下界。我们永远无法证明任何类型都是列表的类型参数的子类型。因此,对于方法<T> takeSuper(List<? super T>),再次证明没有办法获得类型为T的引用。

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

为什么不能将<?><? 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);

CCD_ 48上没有边界;它可以是任何类型。虽然unbound的类型参数是未知,但它确实具有某些类型,并且由于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)的方法,这似乎是错误的。

但CCD_ 57不能推断为CCD_。如果您明确指定Long作为类型参数,您会发现它不起作用:

Main.<Long>takeExtend(superBound);

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

  • 为什么不能将<?><? extends S>传递给期望<? super T>的函数?(为清楚起见,更正了问题:?扩展S

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

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

这是关于推理的。你可以传递一个<?>,因为编译器可以从中生成一些东西。也就是说,它可以从T中生成Object

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

同样,现在要推断上限,这样编译器就可以默认为Object。它对什么不感兴趣?是超级的,也不是超级的。

我整晚都在想这些事情,在你的评论的惊人帮助下,试图找到一个容易记住的解决方案。

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

  • 如果我们通过一个<? extends T>,则上界已经定义,并且会用那个
  • 如果我们通过<?>,我们什么都不知道,但我们知道每个类都扩展Object,因此编译器将推断出Object
  • 如果我们通过<? super T>,我们知道下界,但由于每个类扩展对象,编译器推断出Object

接受<? super T>的方法要得到T,必须推断出该层次结构的下限。在Java中,默认情况下,我们可以指望上界,但不能指望下界。因此,上一个案例中的考虑不再有效。因此:

  • 如果我们通过一个<? extends T>,就没有下界,因为层次结构可以任意扩展
  • 如果我们通过CCD_ 80,没有下限,因为层次结构可以扩展为我们想要多少就多少
  • 如果我们通过一个<? super T>,则下界已经是定义,所以我们可以使用它

重述:

  • 接受CCD_ 82的方法总是依赖于以下事实存在一个上界,即Object->我们可以将每个上界传递给它
  • 接受<? super T>的方法只有在参数中明确定义了下界时才能推断出下界->我们只能传递<? super T>

最新更新