l 尝试理解泛型中的通配符,我有疑问 列表<? super Number >
可以引用任何对象列表并添加任何对象扩展 数字到此列表中,但 l 不能添加到其中 对象不扩展 数字(字符串) 但是为什么我可以在此代码中执行此操作,而不会在运行时出现任何编译错误或异常(参考列表包含字符串对象)
编辑:我想了解泛型提供编译时间安全,这在我的示例中没有实现
List <? super Object> objectList = new ArrayList<>();
objectList.add("str1");
List<? super Number> numberList = objectList;
numberList.add(1);
objectList.add("str2");
for (int i = 0; i < objectList.size(); i++) {
System.out.println(objectList.get(i) + "");
}
你有两种不同类型的多态性在这里以一种令人困惑的方式相互作用。
理解这一点的关键是,除了参数多态性(即泛型)之外,您还有子类型多态性,即经典的面向对象的"is-a"关系。
在 Java 中,所有对象都是Object
的子类型。因此,可以包含Object
值的容器可以包含任何值。
如果我们将所有泛型边界重写为仅<Object>
那么代码的工作方式相同,显然如此:
List<Object> objectList = new ArrayList<>();
objectList.add("str1");
List<Object> numberList = objectList;
numberList.add(1);
objectList.add("str2");
for (int i = 0; i < objectList.size(); i++) {
System.out.println(objectList.get(i) + "");
}
具体来说,objectList.get(i) + ""
被评估为调用objectList.get(i).toString()
的东西,并且由于toString()
是一种Object
方法,因此无论objectList
中的对象类型如何,它都将起作用。
行不通的是:
Number number = numberList.get(i); // error!
这是因为,尽管名称具有误导性,但numberList
不能保证只包含Number
对象,实际上可能根本不包含任何Number
对象!
让我们走一遍,看看为什么必须这样。
首先,我们创建一个对象列表:
List<? super Object> objectList = new ArrayList<>();
这种类型是什么意思?类型List<? super Object>
的意思是"某种类型的对象列表,我不能告诉你是什么类型,但我知道无论它是什么类型,它要么是Object
,要么是Object
的超类型"。我们已经知道Object
是子类型层次结构的根,因此这实际上与List<Object>
相同:也就是说,此对象只能包含Object
对象。
但。。。这不太对。该列表只能包含Object
对象,但Object
对象可以是任何对象!runtype 中的实际对象可以是任何类型,它是Object
的子类型(因此,除了基元之外的任何类型),但是将它们放入此列表中,您将失去判断它们是什么类型的对象的能力 - 它们可以是任何东西。不过,对于该程序的其余部分来说,这是可以的,因为它需要做的只是在对象上调用toString()
,它可以做到这一点,因为它们都扩展了Object
。
现在让我们看一下另一个变量声明:
List<? super Number> numberList = objectList;
同样,类型List<? super Number>
是什么意思?至关重要的是,它的意思是"某种类型的对象列表,我不能告诉你它是什么类型,但我知道无论它是什么类型,它要么是Number
,要么是某种超Number
"。好吧,在左边我们有一个"Number
或Number
的某种超类型"的列表,在右边我们有一个Object
的列表 - 显然Object
是Number
的超类型,所以这个列表是Object
的列表。所有类型都检查(并且,与我最初的评论相反,没有任何警告)。
那么问题就变成了:为什么List<? super Number>
可以包含String
?因为List<? super Number>
可以只是一个List<Object>
,而List<Object>
可以包含一个String
String
因为是一个Object
。
类型List<? super Number>
的引用可以引用List<Object>
或List<Number>
。通过此引用的任何操作都需要使用这些类型中的任何一种。您无法通过List<? super Number>
引用添加字符串,因为该操作仅适用于一种可能的对象类型,但您可以通过List<? super Object>
引用添加字符串。
List<? super Object>
只能指List<Object>
。List<? super Number>
可以指List<Number>
或List<Object>
。这是一种更通用的类型,这就是允许分配的原因。
当编译器看到:
List<? super Number> numberList = objectList;
它首先捕获通配符。然后,泛型类型变为 Y = X>数字(表示 Number 的具体超类型)。所以我们有:
List<Y> numberList = objectList //with type of List<Object>;
然后编译器确定Y可以替换为Object。因此,类型是相同的,numberList
允许指向与objectList
相同的对象。
然后将生成的字节码传递给运行时系统执行。就运行时系统而言,由于类型擦除,这两个列表都具有java.util.ArrayList
的类型。因此,将字符串或其他对象放入此容器时,不会引发运行时异常。
但我也觉得这里有些不太对劲。改写您的问题:
编译器可以做些什么来防止这种情况?
请注意,编译器不得在分配期间抱怨,因为:
安全实例化原则:使用满足参数声明约束的类型实例化参数类应 不会导致错误。
我认为这个原则也适用于作业。赋值不会违反任何语言规则,因此编译器不得引发错误。
因此,唯一能将程序员从灾难中拯救出来的地方就是在add
操作期间。但是编译器可以在那里做什么?如果由于分配而不允许对objectList
执行add
操作,则会违反其他语言规则。如果它增加了add
以支持将对象添加到numberList
,那也将违反其他一些语言规则。
我想不出任何简单明了的解决方案,它不会破坏很多东西来修复甚至可能不是问题的东西,程序员当然处于一个很好的位置来决定。
类型检查器旨在帮助程序员不替换她。其不完美的另一个例子:
public static void main(String[] args) {
Object m = args;
String[] m2 = m; //complains, despite m2 definitely being an String[]
}
PS:我在SO上找到了上面的例子,但不幸的是,我丢失了链接!
这是 java 泛型中的下界通配符功能。
根据 Java 文档,下限通配符将未知类型限制为该类型的特定类型或超类型。
最初,您正在创建一个类型为"对象"或"对象"超类型的列表。如您所知,在java中,每个类都将Object作为超类。因此,我们可以将字符串类作为 Object 的实例。由于您的列表允许对象类型,因此它也可以允许字符串类型。
List<? super Number> numberList = objectList;
也是如此,但你不能反之亦然。
请参阅以了解有关下限通配符的更多了解: Java 下限通配符 https://docs.oracle.com/javase/tutorial/java/generics/lowerBounded.html