为什么我super
只能与通配符一起使用而不能与类型参数一起使用?
比如在Collection
接口中,为什么toArray
方法不是这样写的
interface Collection<T>{
<S super T> S[] toArray(S[] a);
}
为什么我super
只能与通配符一起使用而不能与类型参数一起使用?
比如在Collection
接口中,为什么toArray
方法不是这样写的
interface Collection<T>{
<S super T> S[] toArray(S[] a);
}
super
绑定命名类型参数(例如<S super T>
)而不是通配符(例如<? super T>
)是非法的,因为即使它被允许,它也不会做你希望它做的事情,因为它Object
是所有引用类型的终极super
,而一切都是一个Object
,实际上没有界限。
在您的具体示例中,由于任何引用类型的数组都是Object[]
(通过Java数组协方差),因此它可以<S super T> S[] toArray(S[] a)
在编译时用作(如果这种绑定是合法的)的参数,并且它不会ArrayStoreException
在运行时阻止 -时间。
您要提出的建议是:
List<Integer> integerList;
并给出这个假设 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!
并且没有其他数组类型参数(因为Integer
只有这 3 种类型作为super
)。也就是说,您正试图阻止它编译:
integerList.toArray(new String[0]) // trying to prevent this from compiling
因为,根据您的论点,String
不是super
. 但是,is a of和 a is an ,所以编译器仍然会让上面的代码编译,即使假设你可以这样做!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)
, 但 NOT add(aString)
。好吧,String
是一个Object
,所以add(aString)
仍然会编译。
关于泛型类型规则:
List<Animal> animals = new ArrayList<Dog>()
?List
与原始类型有何List<Object>
不同List<?>
关于使用super
and extends
:
Java Generics: What is PECS?
extends
消费者super
”super
Java泛型和extends
Java泛型有什么区别<E extends Number>
和 和有什么不一样<Number>
?List<? extends Number>
数据结构?(你不能!)由于没有人提供令人满意的答案,正确的答案似乎是“无缘无故”。
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
的类型和您提供的类型。
您的问题的“官方”答案可以在Sun/Oracle 错误报告中找到。
BT2:评估
看
http://lampwww.epfl.ch/~odersky/ftp/local-ti.ps
尤其是第 3 节和第 9 页的最后一段。在子类型约束的两侧承认类型变量可能会导致一组类型方程没有单一的最佳解;因此,类型推断不能使用任何现有的标准算法来完成。这就是类型变量只有“扩展”边界的原因。
另一方面,通配符不必被推断,因此不需要此约束。
@###.### 2004-05-25
是的; 关键是通配符,即使被捕获,也仅用作推理过程的输入;因此,不需要推断(仅)具有下限的任何内容。
@###.### 2004-05-26
我看到了问题。但我看不出它与推理期间通配符下限的问题有何不同,例如:
列表<? 超号>s;
布尔值 b;
...
s = b ?小号:小号;目前,我们推断 List<X> where X extends Object 作为条件表达式的类型,这意味着赋值是非法的。
@###.### 2004-05-26
可悲的是,谈话到此结束。(现已失效的)链接用于指向的论文是Inferred Type Instantiation for GJ。从最后一页看,它归结为:如果允许下限,则类型推断可能会产生多个解决方案,其中没有一个是主要的。
假设我们有:
基本等级 A > 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);
}
好多了,任何带有 U 的代码都在 execute2
超级U型变成了名字!
有趣的讨论:)
我真的很喜欢被接受的答案,但我想对它提出一个稍微不同的看法。
super
在类型化参数中受支持只是为了允许逆变功能。当谈到协变和逆变时,重要的是要了解 Java 仅支持使用点变化。与 Kotlin 或 Scala 不同,它们允许声明站点变化。Kotlin 文档在这里很好地解释了它。或者,如果您更喜欢 Scala,这里有一款适合您。
这基本上意味着在 Java 中,当你根据 PECS 声明它时,你不能限制你将使用你的类的方式。该类既可以消费也可以生产,并且它的一些方法可以同时做到这一点,toArray([])
顺便说一下。
现在,extends
允许在类和方法声明中的原因是因为它更多的是关于多态性而不是关于方差。多态性通常是 Java和OOP 的内在组成部分:如果一个方法可以接受某个超类型,则始终可以安全地将子类型传递给它。如果一个方法,在声明站点作为它的“合同”,应该返回一些超类型,如果它在它的实现中返回一个子类型而不是完全没问题