为什么我只能将super
用于通配符而不用于类型参数?
例如,在Collection
界面中,为什么toArray
方法不是这样编写的
interface Collection<T>{
<S super T> S[] toArray(S[] a);
}
答案 0 :(得分:47)
super
绑定一个命名类型参数(例如<S super T>
)而不是通配符(例如<? super T>
) ILLEGAL 只是因为即使它被允许,它不会做你希望它会做的事情,因为Object
是所有引用类型的最终super
,并且一切都是Object
,有效没有约束。
在您的具体示例中,由于任何引用类型数组是Object[]
(通过Java数组协方差),因此它可以用作<S super T> S[] toArray(S[] a)
的参数(如果这样的约束是合法的)在编译时,它不会在运行时阻止ArrayStoreException
。
你想要提出的建议是:
List<Integer> integerList;
并且在<{1}}上绑定假设的 super
:
toArray
编译器应该只允许以下编译:
<S super T> S[] toArray(S[] a) // hypothetical! currently illegal in Java
并且没有其他数组类型参数(因为integerList.toArray(new Integer[0]) // works fine!
integerList.toArray(new Number[0]) // works fine!
integerList.toArray(new Object[0]) // works fine!
只有{3}类型的这3种类型。也就是说,您正试图阻止编译:
Integer
因为,根据您的论点,super
不是integerList.toArray(new String[0]) // trying to prevent this from compiling
的{{1}}。 但是,String
是super
Integer
,Object
是super
,因此编译器仍然会让上面的编译,即使假设你可以做Integer
!
所以下面的仍然会编译(就像它们现在的方式一样),并且使用泛型类型的任何编译时检查都无法阻止运行时的String[]
界限:
Object[]
泛型和数组不会混合,这是它显示的许多地方之一。
再次,假设你有这个通用的方法声明:
<S super T>
你有这些变量声明:
ArrayStoreException
您对integerList.toArray(new String[0]) // compiles fine!
// throws ArrayStoreException at run-time
(如果合法)的意图是它应该允许<T super Integer> void add(T number) // hypothetical! currently illegal in Java
和Integer anInteger
Number aNumber
Object anObject
String aString
,当然还有<T super Integer>
,但不是add(anInteger)
。好吧,add(aNumber)
是add(anObject)
,所以add(aString)
仍然可以编译。
关于泛型类型规则:
List<Animal> animals = new ArrayList<Dog>()
? String
与Object
的区别与add(aString)
不同使用List
和List<Object>
:
Java Generics: What is PECS?
List<?>
consumer super
”super
and extends
in Java Generics <E extends Number>
and <Number>
? List<? extends Number>
data structures?(你不能!)答案 1 :(得分:29)
由于没有人提供满意的答案,正确答案似乎是“无缘无故”。
polygenelubricants提供了对java数组协方差发生的不良事件的一个很好的概述,这本身就是一个可怕的特征。请考虑以下代码片段:
String[] strings = new String[1];
Object[] objects = strings;
objects[0] = 0;
这显然错误的代码编译时没有求助于任何“超级”构造,因此不应将数组协方差用作参数。
现在,我在命名类型参数中有一个非常有效的代码示例,需要super
:
class Nullable<A> {
private A value;
// Does not compile!!
public <B super A> B withDefault(B defaultValue) {
return value == null ? defaultValue : value;
}
}
可能支持一些不错的用法:
Nullable<Integer> intOrNull = ...;
Integer i = intOrNull.withDefault(8);
Number n = intOrNull.withDefault(3.5);
Object o = intOrNull.withDefault("What's so bad about a String here?");
如果我完全删除B
,则后一代码片段无法编译,因此确实需要B
。
请注意,如果我反转类型参数声明的顺序,则很容易获得我尝试实现的功能,从而将super
约束更改为extends
。但是,只有将该方法重写为静态方法时才能实现这一点:
// This one actually works and I use it.
public static <B, A extends B> B withDefault(Nullable<A> nullable, B defaultValue) { ... }
关键是这种Java语言限制确实限制了一些其他可能有用的功能,可能需要丑陋的解决方法。我想知道如果我们需要withDefault
是虚拟的,会发生什么。
现在,为了与polygenelubricants所说的相关联,我们在这里使用B
不要限制作为defaultValue
传递的对象的类型(参见示例中使用的字符串),而是限制调用者对我们返回的对象的期望。作为一项简单的规则,您可以将extends
与您要求的类型一起使用,并将super
与您提供的类型一起使用。
答案 2 :(得分:11)
您的问题的“官方”回答可以在Sun/Oracle bug report中找到。
可悲的是,谈话在那里结束了。 (现在已死)链接用于指向的论文是Inferred Type Instantiation for GJ。通过浏览最后一页,它归结为:如果允许下限,则类型推断可能会产生多个解,其中没有一个是principal。BT2:评价
见
http://lampwww.epfl.ch/~odersky/ftp/local-ti.ps
特别是第3节和第9页的最后一段。承认 子类型约束两侧的类型变量可以导致a 没有单一最佳解的类型方程组;所以, 使用任何现有标准都无法进行类型推断 算法。这就是为什么类型变量只有“扩展”边界。
另一方面,通配符不必推断,所以那里 不需要这种约束。
@ ###。### 2004-05-25
是;关键点在于,只有使用通配符,即使在捕获时也是如此 作为推理过程的输入;没有(仅)下限需求 作为结果推断出来。
@ ###。### 2004-05-26
我看到了问题。但我不明白它与问题的区别 我们在推理过程中使用了通配符的下限,例如:
列表与LT ;?超级数字&gt; S;
布尔b;
...
s = b? s:s;目前,我们推断List&lt; X&gt;其中X扩展Object作为类型 条件表达式,意味着赋值是非法的。
@ ###。### 2004-05-26
答案 3 :(得分:-1)
假设我们有:
基本类A&gt; B> C和D
class A{
void methodA(){}
};
class B extends A{
void methodB(){}
}
class C extends B{
void methodC(){}
}
class D {
void methodD(){}
}
作业包装类
interface Job<T> {
void exec(T t);
}
class JobOnA implements Job<A>{
@Override
public void exec(A a) {
a.methodA();
}
}
class JobOnB implements Job<B>{
@Override
public void exec(B b) {
b.methodB();
}
}
class JobOnC implements Job<C>{
@Override
public void exec(C c) {
c.methodC();
}
}
class JobOnD implements Job<D>{
@Override
public void exec(D d) {
d.methodD();
}
}
和一个管理器类,有4种不同的方法来执行对象上的作业
class Manager<T>{
final T t;
Manager(T t){
this.t=t;
}
public void execute1(Job<T> job){
job.exec(t);
}
public <U> void execute2(Job<U> job){
U u= (U) t; //not safe
job.exec(u);
}
public <U extends T> void execute3(Job<U> job){
U u= (U) t; //not safe
job.exec(u);
}
//desired feature, not compiled for now
public <U super T> void execute4(Job<U> job){
U u= (U) t; //safe
job.exec(u);
}
}
使用
void usage(){
B b = new B();
Manager<B> managerB = new Manager<>(b);
//TOO STRICT
managerB.execute1(new JobOnA());
managerB.execute1(new JobOnB()); //compiled
managerB.execute1(new JobOnC());
managerB.execute1(new JobOnD());
//TOO MUCH FREEDOM
managerB.execute2(new JobOnA()); //compiled
managerB.execute2(new JobOnB()); //compiled
managerB.execute2(new JobOnC()); //compiled !!
managerB.execute2(new JobOnD()); //compiled !!
//NOT ADEQUATE RESTRICTIONS
managerB.execute3(new JobOnA());
managerB.execute3(new JobOnB()); //compiled
managerB.execute3(new JobOnC()); //compiled !!
managerB.execute3(new JobOnD());
//SHOULD BE
managerB.execute4(new JobOnA()); //compiled
managerB.execute4(new JobOnB()); //compiled
managerB.execute4(new JobOnC());
managerB.execute4(new JobOnD());
}
有关如何实现execute4的任何建议吗?
==========编辑=======
public void execute4(Job<? super T> job){
job.exec( t);
}
感谢所有人:)
==========编辑==========
private <U> void execute2(Job<U> job){
U u= (U) t; //now it's safe
job.exec(u);
}
public void execute4(Job<? super T> job){
execute2(job);
}
更好,任何代码都在exe2中使用了
超级型U被命名为!
有趣的讨论:)答案 4 :(得分:-1)
我真的很喜欢这个被接受的答案,但是我希望对此有一个稍微不同的看法。
类型参数中支持 super
只是为了允许 contravariance 功能。关于协方差和 contravariance ,了解Java仅支持使用地点差异非常重要。与Kotlin或Scala不同,后者允许声明站点差异。 Kotlin文档对此进行了很好的解释here。或者,如果您更喜欢Scala,here就是您的最佳选择。
这基本上意味着在Java中,当根据PECS声明类时,您不能限制使用类的方式。该类既可以消费也可以产生,顺便说一下,它的某些方法可以同时进行,例如toArray([])
。
现在,在类和方法声明中允许使用extends
的原因是,它更多地是关于多态性而不是关于变异性。而多态性通常是Java和OOP的固有部分:如果方法可以接受某些超类型,则始终可以将子类型安全地传递给它。而且,如果某个方法(在声明站点中是“合同”)应返回某个超类型,则在其实现中返回子类型代替它完全没问题