为什么列表<?超级数字>可以包含字符串



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"。好吧,在左边我们有一个"NumberNumber的某种超类型"的列表,在右边我们有一个Object的列表 - 显然ObjectNumber的超类型,所以这个列表是Object的列表。所有类型都检查(并且,与我最初的评论相反,没有任何警告)。

那么问题就变成了:为什么List<? super Number>可以包含String?因为List<? super Number>可以只是一个List<Object>,而List<Object>可以包含一个StringString因为是一个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

最新更新