Java泛型问题

3
以下代码可以编译通过,但是如果我取消注释掉的那一行,它就不能编译通过了,我感到困惑。HashMap确实继承了AbstractMap, 在声明map的第一行也可以编译通过。
import java.util.AbstractMap;
import java.util.HashMap;
import java.util.Map;

public class Test {

    public static void main(String args[]) {
        Map<String, ? extends AbstractMap<String, String>> map = new HashMap<String, HashMap<String, String>>();
        //map.put("one", new HashMap<String, String>());
    }
}

我知道“正确的方法”是这样的:

import java.util.HashMap;
import java.util.Map;

public class Test {

    public static void main(String args[]) {
        Map<String, Map<String, String>> map = new HashMap<String, Map<String, String>>();
        map.put("one", new HashMap<String, String>());
    }
}
3个回答

7
第一段代码是不安全的 - 想象一下你实际上写的是:
HashMap<String, ConcurrentHashMap<String, String>> strongMap = 
    new HashMap<String, ConcurrentHashMap<String, String>>();
Map<String, ? extends AbstractMap<String, String>> map = strongMap;

现在:

map.put("one", new HashMap<String, String>());
ConcurrentHashMap<String, String> x = strongMap.get("one");

我们应该有一个ConcurrentHashMap,但实际上我们只有一个HashMap。

如果我们减少通用性的使用,这个问题就可以简单地解释...你的情景真正等同于(比如说):

List<? extends Fruit> list = new List<Apple>();
list.add(new Apple());

这段代码看起来没问题,但是如果你考虑到它在编译器看来和以下代码等价:

List<Apple> apples = new ArrayList<Apple>();
List<? extends Fruit> list = apples;
list.add(new Orange());
Apple apple = list.get(0); // Should be okay... but element 0 is an Orange!

这显然是不可以的。编译器必须以相同的方式处理这两个内容,所以它将两者都视为无效。


嗨Jon,你能解释一下吗?为什么不安全?在我看来,橙子的例子应该是可以工作的。它应该会将一个橙子添加到水果列表中。 - Bick
最重要的是,如果我们想要持有一个通用的水果列表,该怎么办? - Bick
@user450602:但它实际上不是水果列表 - 它是一个List<Apple>。由于类型擦除,Java无法识别,但这就是它创建的方式。我将编辑示例以显示更多为什么它不应该工作。 - Jon Skeet
@user450602:这取决于你所说的“一个通用水果列表”的含义 - 如果你指的是一个可以添加任何水果的列表,并且只有在后来才知道里面是什么水果,那么你只需要使用List<Fruit> list = new ArrayList<Fruit>(); - Jon Skeet
2
@user450602:对于你不需要的情况。例如,如果你将一个List<? extends Fruit>传递到一个方法中,它可以获取任何值并知道它是一个Fruit,这可能是它所需要的全部。如果你传递一个List<? super Fruit>,那么它知道它可以添加任何水果,但不知道它可以从列表中获取什么类型。 - Jon Skeet

0
除了Jon的优秀答案外,还可以参考这个关于PECS的问题,它涵盖了很多相同的内容。

0

如果你想要真正的技术解释,我建议你阅读这些幻灯片中基于组件的软件,以完整地了解这个问题,因为你的小问题在大规模上有着巨大的影响 :)

真正的术语是逆变协变。在这些幻灯片中搜索关于面向对象设计及其在更大系统中的限制。

你的问题是这些问题与一些泛型混合而成 :)

也就是说,你的契约(比水果更具体的东西的产物)指定列表必须能够保留所有种类的水果,但你创建的列表只能容纳某种特定类型的水果(苹果或比苹果更具体的东西),根据我的看法,这应该引发编译器错误,但Java编译器太好了,而且Java中的泛型没有得到很好的实现(从语义和内省的角度来看)。

容器实例可以是协变或不变的,与容器类型/类相对应,但实例的包含者必须是不变的,与包含者类型/类相对应。

一个具体的例子:

List<Fruit> list = new ArrayList<Fruit>();

一个通用的例子:

ConatainerType<ElementOfList> list = new MoreSpecificContainerType<ElementOfList>();

ElementOfList 必须同时满足协变性和逆变性,因为对象既可以被放入(协变),也可以被检索(逆变),这只留下了不变性,即相同的类型/类。

这是一个非常长的答案,但我希望它能帮助未来有类似问题的人。


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