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>