为什么List <!-?super Number->可以包含字符串

时间:2018-10-31 18:28:35

标签: java generics

我试图了解泛型中的通配符,并且我有疑问 List <? super Number >可以引用任何对象列表,并可以将任何扩展了Number的对象添加到该列表中,但是我不能添加到对象中,不能扩展number(String) 但是为什么我可以在这段代码中做到这一点而在运行时没有任何编译错误或异常(指的是包含String对象的列表)

编辑:我想了解泛型提供了编译时安全,而在我的示例中没有实现

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) + "");
} 

4 个答案:

答案 0 :(得分:4)

您以一种令人困惑的方式在这里进行交互的两种不同的多态性。

了解这一点的关键是,除了参数多态性(即泛型)之外,您还具有子类型多态性,即经典对象面向的“是”关系。

在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的一种方法,因此无论{中的对象类型如何, {1}}。

这是行不通的:

objectList

这是因为,尽管名称具有误导性,但不能保证Number number = numberList.get(i); // error! 仅包含numberList个对象,并且实际上可能根本不包含任何Number个对象!

我们来看一下为什么一定要这么做。

首先,我们创建一个对象列表:

Number

该类型是什么意思?类型List<? super Object> objectList = new ArrayList<>(); 的意思是“某种类型的对象的列表,我无法告诉您什么类型,但是我知道无论它是List<? super Object>还是{{1}的超类型}”。我们已经知道Object是子类型层次结构的根,因此它实际上与Object相同:也就是说,该对象只能包含Object个对象。

但是...那是不对的。该列表只能包含List<Object>个对象,但是Object对象可以是任何对象! runtype的实际对象可以是Object的子类型的任何类型(因此,除基本类型以外的任何其他类型),但是通过将它们放入此列表,您将失去分辨哪种对象的能力。它们已经了-它们可以是任何东西。不过,对于该程序的其余部分来说,这是可以的,因为它所需要做的就是在对象上调用Object,并且因为它们都扩展了Object,所以可以这样做。

现在让我们看看另一个变量声明:

toString()

同样,类型Object是什么意思?至关重要的是,它的意思是“某种类型的对象的列表,我无法告诉您它是什么类型,但是我知道无论它是什么类型,它要么是List<? super Number> numberList = objectList; 要么是List<? super Number>的某种超类型”。好吧,在左侧,我们有一个列表“ Number或某些Number的超类型,在右侧,我们有一个列表Number-显然是NumberObject的超类型,因此此列表是Object的列表。一切类型检查(并且与我最初的评论相反,没有任何警告)。

问题就变成了:为什么Number可以包含Object?因为List<? super Number>只能是String,而List<? super Number>可以包含List<Object>,因为List<Object>是-String

答案 1 :(得分:2)

类型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>。这是一种更通用的类型,这就是为什么允许分配的原因。

答案 2 :(得分:1)

这是Java泛型中的下界通配符功能。

根据Java文档,下界通配符将未知类型限制为特定类型或该类型的超类型。

最初,您正在创建一个对象类型或对象超级类型的列表。如您所知,在Java中,每个类都有Object作为超类。因此我们可以将String类作为Object的实例。由于您的列表允许使用Object类型,因此也可以允许使用String类型。

List<? super Number> numberList = objectList;也是如此,但是反之亦然。

请参阅以获取有关下限通配符的更多信息: Java lower bound wildcards https://docs.oracle.com/javase/tutorial/java/generics/lowerBounded.html

答案 3 :(得分:1)

当编译器看到以下内容时:

List<? super Number> numberList = objectList;

首先captures通配符。然后,泛型变为Y = X > Number (意味着Number的具体超类型)。所以我们有:

List<Y> numberList = objectList //with type of List<Object>;

然后,编译器确定可以用 Object 替换 Y 。因此,类型相同,并且允许numberList指向与objectList相同的对象。

然后将生成的字节码传递到运行时系统以执行。就运行时系统而言,由于type erasure,两个列表的类型均为java.util.ArrayList。因此,将字符串或其他对象放入此容器时,不会引发运行时异常。

但是我也觉得这里有些不对劲。重述您的问题:

编译器可以采取什么措施来防止这种情况?

请注意,编译器在分配because期间不得抱怨:

  

安全实例化原理:实例化具有满足对参数声明的约束的类型的参数类应   不会导致错误。

我认为这项原则也适用于作业。分配不会违反任何语言规则,因此编译器不得引发错误。

因此,唯一可以使程序员免于灾难的地方是在add操作期间。但是编译器在那里可以做什么?如果由于分配而不允许在add上进行objectList操作,则会违反其他语言规则。如果它增加了add来支持向numberList添加对象,那还将违反其他一些语言规则。

我想不出任何简单易用的解决方案,不会解决很多问题,而这些问题甚至可能都不是问题,程序员肯定处于决定的位置。

类型检查器旨在帮助程序员不要替换她。不完美的另一个例子:

public static void main(String[] args) {
    Object m = args;
    String[] m2 = m; //complains, despite m2 definitely being an String[]
}

PS:我在SO上找到了上面的示例,但不幸的是,我失去了链接!