为什么带有"<? super ArrayList>"泛型声明的HashMap在put方法中不接受值"new Object()"?

12

在准备面试题时,我遇到了下面的代码:

List<Object> list = new ArrayList();
Map<Object, ? super ArrayList> m = new HashMap<Object,  ArrayList>();
m.put(1, new Object());
m.put(2, list);

上述的两个put方法会抛出编译时错误。但是,当我添加m.put(3, new ArrayList());时,它会将其添加到Map中而不会有编译时错误。

对于我来说非常清楚,我可以将new Object()作为值添加到HashMap中,因为映射声明为< ? super ArrayList>类型;这意味着我可以添加任何高于 ArrayList (即ArrayListsuper)的值和ArrayList对象本身,但不能添加低于ArrayList的任何值。

这个特定的概念在Kathy Sierra和Bert Bates的SCJP 6中写得非常好,并且基于该理论和示例,我认为它应该按照我的理解执行。请问有人能帮我理解这个错误吗?


在我终于理解了这个问题的工作原理之后,我又添加了另一个答案,因为我仍然认为其他答案都没有很好地解释清楚。 - wvdz
10个回答

8
你误解了通配符?的含义,可能存在一个常见的误解:
它并不意味着你可以将任何类型的对象放入映射中,这些对象是ArrayList的超类。
它的意思是映射中的值是一些未知类型,这些类型是ArrayList的超类型。由于确切的类型是未知的,编译器不允许你将除ArrayList本身以外的任何类型的值作为映射中的值 - 编译器没有足够的信息来检查你所做的是否类型安全。
假设这是被允许的,那么你可能会做出糟糕的事情,比如:
Map<Object, ArrayList> m1 = new HashMap<Object, ArrayList>();

Map<Object, ? super ArrayList> m2 = m1;

// This should not be allowed, because m2 is really a HashMap<Object, ArrayList>
m2.put(1, new Object());

我仍然不理解。从你在这里所说的来看,甚至没有超级通用关键词的用例。 - wvdz
@Jesper是错的,<? super X>的意思是X或更大;请查看我新增的示例答案。 - ThanksForAllTheFish
3
(现在正在电脑上)...超级关键字有一个巨大的用例。请查看Joshua Bloch的书《Effective Java(第二版)》,其中他描述了PECS,它代表“生产者使用extends,消费者使用super”。这个SO答案可能会为您提供更多的见解:https://dev59.com/7nE85IYBdhLWcg3wikK- - lostdorje
这个答案基本上就像在瞎子国度里的有眼之王。在所有可怕的回答中,这可能是最不可怕的,但它仍然没有提供如何超级泛型关键字工作(和不工作)的适当解释。 - wvdz
1
您IP地址为143.198.54.68,由于运营成本限制,当前对于免费用户的使用频率限制为每个IP每72小时10次对话,如需解除限制,请点击左下角设置图标按钮(手机用户先点击左上角菜单按钮)。 - Oliver Charlesworth

3
? super ArrayList的意思是“我不确定这个类型的确切类型,但是任何需要提供此类型实例的地方,我可以安全地提供ArrayList”。实例化通配符的实际类型可能是ObjectCollectionListArrayList,因此您可以看到new Object()不能保证是安全的,但new ArrayList()是安全的。

3
类型 ? super ArrayList 表示未知类型,其下限为 ArrayList。例如,它可能是 Object,但也可能是 AbstractListArrayList
编译器唯一能确定的是值的类型,虽然不知道具体是什么类型,但保证不会比 ArrayList 更特定,因此可以添加任何是 ArrayList 或其子类的对象。
同样要记住:编译器只关注声明类型,而不理会赋值类型。

为什么 put(1, new Object()) 失败:

显然,Object 不在范围内。

为什么 put(1, list) 失败:

变量的类型是 List,可以持有对 LinkedList 的引用,而 LinkedList 不符合所需的范围。


因此,任何一个是ArrayList或ArrayList子类的对象都可以添加进去。那么,“? extends ArrayList”的区别在哪里呢? - wvdz
1
<? extends ArrayList> 的意思是你不知道它的类型,但你知道从对象中获取的类型至少与ArrayList一样具体(即ArrayList或ArrayList的子类型),并且你不能添加任何东西,因为你不知道类型可能是ArrayList的哪个子类。请参见PECS - Bohemian
我就是无法理解它。我打算弄到一本《Effective Java》的副本... - wvdz
如果你有一个 List<? extends T>,那么这个 List 最多是一个 List<T>,也可能是 T 的某个子类。如果你有一个 List<? super T>,那么这个 List 最少是一个 List<T>,也可能是 T 的某个超类。这个边界告诉你关于曾经存在的泛型参数的一些信息。 - Radiodef
@Radiodef,是的,我现在终于明白了,我甚至添加了自己的答案,好像还不够多:p - wvdz

2

? super ArrayList并不意味着“任何高于ArrayList的值”。它的意思是“某种未知类型?的任何值,它是ArrayList的超类型”。实际上,在你的代码中可以看到这一点,因为m的值是ArrayList类型:

HashMap<Object,  ArrayList> hm = new HashMap<Object,  ArrayList>();
Map<Object, ? super ArrayList> m = hm;
m.put(1, new Object());
ArrayList a = hm.get(1);

这段代码明显有问题,因为 a 应该是一个 ArrayList,而不是一个 Object


2

Map<Object, ? super ArrayList> m可以被许多地图引用,例如:

  • HashMap<Object, ArrayList>
  • HashMap<Object, List>
  • HashMap<Object, Object>

但这些地图都有自己的目的,即精确地保存这些类型的数据:

  • ArrayList
  • List
  • Object

或它们的子类型,但从不使用它们的超类型,因为超类型可能没有所有必要的成员(方法/字段),而它们的子类型具有这些成员。

考虑以下示例:

List<Banana> bananas = new ArrayList<>();//list only for bananas    <------+
List<? super Banana> list = bananas;//this is fine                         |
//                                                                         |
list.add(new Object);//error, and can thank compiler for that              |
                     //because you just tried to add Object to this list --+
                     //which should contain only Bananas
list.add(new Banana());// this is FINE because whatever precise type of list
                       // it can accept also banana

为了说明上面的评论,让我们使用以下内容:
List<Fruit> fruits = new ArrayList<>();//can contain Bananas, Apples, and so on
List<? super Banana> list = fruits;//again, this is fine                         
list.add(new Object)//wrong, as explained earlier
list.add(new Banana());// OK because we know that list will be of type which
                       // can also accept Banana

2
我相信没有一个答案能够给出真正令人满意的解释。我不得不“跑到图书馆”,阅读Joshua Bloch的《Effective Java》第五章,最终才理解了这个问题的工作原理。
这个问题有些复杂,因为它使用了Map和一个未经处理的ArrayList。这并没有使问题更加清晰。基本上,这个问题是关于以下泛型声明的含义及其区别的:
Collection<? super E> x;
Collection<? extends E> y;

x = 一种未知类型的集合,该类型是E或E的超类。

y = 一种未知类型的集合,该类型是E或E的子类。

x的用例:可以向其中添加E或任何E的子类。

y的用例:可以从中检索E或任何E的子类。

这也被称为PECS缩写:生产者扩展,消费者超级。

如果集合应该能够“消耗”E(E可以添加到其中),则将泛型声明为? super E

如果集合应该能够“产生”E(可以从中检索E),则将泛型声明为? extends E

希望这解释了为什么问题中的代码无法编译:只有ArrayList或其子类型才能添加到映射中。


1
您可以参考Java规范第 4.5.1. Type Arguments and Wildcards 章节,该章节说明:
与方法签名中声明的普通类型变量不同,使用通配符时不需要进行类型推断。因此,可以使用以下语法在通配符上声明下限,其中B是下限:
? super B
下限意味着有效类型是所有“大于或等于”B(在您的情况下为ArrayList)。既不是Object也不是List比ArrayList更大,这就是为什么会出现编译错误的原因。
为了进一步测试这一点,请尝试声明一个新的自定义类型,如public class MyArrayList extends ArrayList,并在您的Map中使用它:第二个put将起作用。
或者将Map声明为Map:同样,第二个put会起作用。

然而,你无法将一个Object放入这样的映射中:因为Object是Java类类型的根,因此它比所有其他类型都要“小”。

这个示例可以编译(使用标准的javac):

import java.util.*;

public class test {
  public static void main(String[] args) {
    ArrayList list = new ArrayList();
    Map<Object, ? super List> m = new HashMap<Object, List>(); 
    m.put(2, list);
  }
}

因此,下限实际上意味着您可以放置大于或等于该值的所有内容。

1
当你输入 < /p >时
Map<Object, ? super ArrayList> m = new HashMap<Object,
    ArrayList>();

你的 "?" 变成了 ArrayList,因此你除了通过继承 ArrayList 来添加元素之外就不能再添加任何东西了。


1
泛型的存在是为了提供这种行为。您可以使用泛型来限制映射条目。您的上限是ArrayList,但下限是开放的,可以是ArrayList的任何子类。因此,您不能将Object条目放入此映射中。

0

你所说的是正确的。当你只是在运行代码时,把所有的ArrayList的超类放入Map作为一个值是有意义的。

但问题是这是设计上的问题。泛型检查仅在编译时而不是运行时进行。因此,在编译时,尽管你知道Object是ArrayList的超类;编译器不知道。所以编译器会对此发出警告。唯一可以作为值放入的是nullArrayList对象本身。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接