【问题标题】:why List<?super Number > can contain String为什么 List<?super Number > 可以包含 String
【发布时间】:2018-10-31 18:28:35
【问题描述】:

我尝试理解泛型中的通配符,但我有疑问 List&lt;? super Number &gt; 可以引用任何对象列表并将任何对象扩展编号添加到此列表中,但我不能添加到其中对象不扩展编号(字符串) 但是为什么我可以在这段代码中做到这一点而在运行时没有任何编译错误或异常(指列表包含字符串对象)

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

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

【问题讨论】:

  • 我猜如果您执行numberList.get(i) + "" 之类的操作,您会遇到问题。您在循环中使用objectList,所以我不认为会有问题。
  • 哎呀我想不出它的正确名称,但实际上边界的方向在这里很重要。但是类型“检查”是在检索时完成的,而不是在您插入对象列表时(无论如何都会通过 Object 类型检查)。
  • @Rogue PECS?
  • List&lt;? super SomeType&gt; 只允许您添加数据(参见What is PECS (Producer Extends Consumer Super)?)。那么为什么让List&lt;? super Number&gt; 处理List&lt;? super Object&gt; 会不安全呢?如果我们知道 List&lt;? super Object&gt; 持有对象列表,那么将 Integer 添加到它应该不是问题,因为持有的 List&lt;Object&gt; (或 List - 如果可能的话)将被声明为持有 Object (包括它的子类型) .
  • 不是完全重复的,但这个问题密切相关:Generics &lt;? super A&gt; doesn't allow supertypes of A to be added to the list

标签: java generics


【解决方案1】:

这里有两种不同的多态性,它们以一种令人困惑的方式相互作用。

理解这一点的关键是,除了参数多态(即泛型),你还有子类型多态,即经典对象面向“is-a”的关系。

在 Java 中,所有对象都是 Object 的子类型。所以一个可以包含Object值的容器可以包含任何值。

如果我们将所有通用边界重写为 &lt;Object&gt;,那么代码的工作方式相同,显然是这样:

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&lt;? super Object&gt; 类型的意思是“某种类型的对象列表,我不能告诉你什么类型,但我知道它是ObjectObject 的超类型”。我们已经知道Object 是子类型层次结构的根,所以这实际上与List&lt;Object&gt; 相同:也就是说,这个对象只能包含Object 对象。

但是……这不太对。该列表只能包含Object 对象,但Object 对象可以是任何东西! runtype 中的实际对象可以是Object 子类型的任何类型(因此,除了原始类型之外的任何类型),但是将它们放入此列表中,您将无法分辨它们是什么类型的对象不再——它们可以是任何东西。不过,对于该程序的其余部分来说,这没关系,因为它所要做的就是在对象上调用toString(),并且它可以这样做,因为它们都扩展了Object

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

List<? super Number> numberList = objectList;

同样,List&lt;? super Number&gt; 类型是什么意思?至关重要的是,它的意思是“某种类型的对象列表,我不能告诉你它是什么类型,但我知道无论它是什么类型,它要么是Number,要么是Number 的某些超类型”。好吧,在左边我们有一个“NumberNumber”的超类型列表,在右边我们有一个Object 列表——显然ObjectNumber 的超类型和所以这个列表是Object 的列表。一切类型检查(并且,与我最初的评论相反,没有任何警告)。

所以问题变成了:为什么List&lt;? super Number&gt; 可以包含String?因为List&lt;? super Number&gt; 可以只是List&lt;Object&gt;,而List&lt;Object&gt; 可以包含String,因为String 是一个Object

【讨论】:

  • @aka-one:因为List&lt;? super Number&gt; 可以是只是List&lt;Object&gt;,但也可以是List&lt;Number&gt;。编译器不知道哪个,所以它必须是保守的。如果它是 List&lt;Number&gt;,那么向它添加 Object 会破坏列表,因此会出现类型错误。
  • @aka-one:没有什么可以禁止List&lt;? super Number&gt; 包含字符串。禁止通过List&lt;? super Number&gt; 变量将字符串添加到列表中。
  • @aka-one:我认为您误解了? 在泛型类型中的含义。这并不意味着“这些类型中的任何一种”,它的意思是“正是这些类型中的一种,但我不会告诉你是哪一种”。它可能NumberSerializableObject 的列表。从本质上讲,List&lt;? super Number&gt; 的意思是“某种类型的列表,我无法告诉你具体是什么类型,但我所知道的只是将Number 添加到其中是安全的”。它没有告诉你如果你从列表中取出一些东西会得到什么。这就是“生产者扩展,消费者超级”的经验法则的含义。
  • 值得记住的是any != allList&lt;? extends Cat&gt; list 表示该列表可以容纳 anynew List&lt;Cat&gt;() new List&lt;Animal&gt;() new List&lt;Object&gt;(),但一次只能容纳 一个(并将 Cat 放入这样的列表应该不是问题)。
  • @HanaaAldaly 也许这会有所帮助。编译器不知道究竟numberList 将持有什么列表。就像void someMethod(List&lt;? super Number&gt; numberList){ numberList.add(...);} 的情况一样,它不知道我们传递给它的是 List 还是 List。为了使该方法安全,它只需要允许对所有这些类型都有效的指令。如果用户将传递ArrayList&lt;Object&gt;,那么确实将字符串添加到它不会对该列表造成任何问题,但是如果用户将作为参数传递List&lt;Number&gt;,那么将字符串添加到它就不安全了。
【解决方案2】:

List&lt;? super Number&gt; 类型的引用可以引用List&lt;Object&gt;List&lt;Number&gt;。通过此引用进行的任何操作都需要使用这些类型中的任何一种。您无法通过 List&lt;? super Number&gt; 引用添加字符串,因为该操作仅适用于其中一种可能的对象类型,但您可以通过 List&lt;? super Object&gt; 引用。

List&lt;? super Object&gt; 只能引用List&lt;Object&gt;List&lt;? super Number&gt; 可以引用 List&lt;Number&gt;List&lt;Object&gt;。这是一种更通用的类型,这就是允许赋值的原因。

【讨论】:

    【解决方案3】:

    当编译器看到时:

    List<? super Number> numberList = objectList;
    

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

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

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

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

    但我也觉得这里有些地方不太对劲。重新表述您的问题:

    编译器可以做些什么来防止这种情况发生?

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

    安全实例化原则:实例化具有符合参数声明约束的类型的参数类应该 不会导致错误。

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

    因此,唯一能将程序员从灾难中拯救出来的地方就是在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 上找到了上面的示例,但不幸的是,我丢失了链接!

    【讨论】:

      【解决方案4】:

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

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

      最初,您正在创建一个具有 Object 类型或超类型 Object 的列表。正如您在 java 中所知道的,每个类都有 Object 作为超类。所以我们可以将String类作为Object的一个实例。由于您的列表允许 Object 类型,它也可以允许 String 类型。

      List&lt;? super Number&gt; numberList = objectList; 也是如此,但反之亦然。

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

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 1970-01-01
        • 2012-12-16
        • 1970-01-01
        • 2014-07-29
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2022-01-09
        相关资源
        最近更新 更多