List<Map<String, String>> 与 List<? extends Map<String, String>> 的区别

131

有什么区别吗?

List<Map<String, String>>

List<? extends Map<String, String>>

如果没有区别,那么使用 ? extends 有什么好处呢?


2
我喜欢Java,但这是其中一件不那么好的事情... - Mite Mitreski
5
我觉得如果我们把它读作“任何扩展……”,那么就更清楚了。 - Garbage
令人难以置信,在大约3天内就有超过12K的浏览量?!! - Eng.Fouad
5
它已经登上了 Hacker News 的首页。恭喜! - Chinmoy
@r3st0r3,你能给我那个页面的链接吗? :) - Eng.Fouad
1
@中文。在这里找到了。不再在首页上,但昨天还在那里。http://news.ycombinator.net/item?id=3751901 (昨天是印度的周日中旬)。 - Chinmoy
5个回答

185
区别在于,例如,a
List<HashMap<String,String>>

是一个

List<? extends Map<String,String>>

但不是a

List<Map<String,String>>

所以:
void withWilds( List<? extends Map<String,String>> foo ){}
void noWilds( List<Map<String,String>> foo ){}

void main( String[] args ){
    List<HashMap<String,String>> myMap;

    withWilds( myMap ); // Works
    noWilds( myMap ); // Compiler error
}

您可能认为List中的HashMap应该是List中的Map,但是这并不是这样的一个很好的原因:

假设您可以执行:

List<HashMap<String,String>> hashMaps = new ArrayList<HashMap<String,String>>();

List<Map<String,String>> maps = hashMaps; // Won't compile,
                                          // but imagine that it could

Map<String,String> aMap = Collections.singletonMap("foo","bar"); // Not a HashMap

maps.add( aMap ); // Perfectly legal (adding a Map to a List of Maps)

// But maps and hashMaps are the same object, so this should be the same as

hashMaps.add( aMap ); // Should be illegal (aMap is not a HashMap)

因此,一个HashMapList不应该是MapList

6
尽管如此,“HashMap”由于多态性仍然是一个“Map”。 - Eng.Fouad
47
没错,但 HashMap 的列表不是 Map 的列表。 - trutheality
1
这被称为有界量词化 - Dan Burton
好的例子。值得注意的是,即使您声明了List<Map<String,String>> maps = hashMaps;HashMap<String,String> aMap = new HashMap<String, String>();,您仍然会发现maps.add(aMap);是非法的,而hashMaps.add(aMap);是合法的。这样做的目的是防止添加错误类型,但它不允许添加正确类型(编译器无法在编译时确定“正确”类型)。 - Raze
@trutheality,糟糕!我想说的是 List<? extends Map<String, String>> maps = ...!!! - Raze
显示剩余4条评论

24

您无法将诸如 List<NavigableMap<String,String>> 等类型的表达式分配给第一个参数。

(如果您想知道为什么不能将 List<String> 分配给 List<Object>,请参见 Stack Overflow 上无数其他问题。)


1
可以再解释一下吗?我不太理解。或者有没有好的练习链接? - Samir Mangroliya
3
“@Samir Explain what? List<String>不是List<Object>的子类型?” - 例如,参见https://dev59.com/E3A75IYBdhLWcg3wg5js - Tom Hawtin - tackline
2
这并没有解释? extends有什么区别,也没有解释与超类/子类型或协变/逆变的相关性(如果有的话)。 - Abel

18
其他答案中缺少一个参考,解释了这与Java中的协变性、逆变性和子类型、超类型(即多态)的关系。如果OP已经理解,那很好,但以防万一,我们来看看:

协变性

如果你有一个类Automobile,那么CarTruck是它们的子类型。任何Car可以分配给类型为Automobile的变量,这在OO中是众所周知的,并称为多态性。协变性是指在泛型或委托的情况下使用相同原理的情况。Java没有委托(尚未),因此该术语仅适用于泛型。

我倾向于将协变性视为标准多态性,你期望无需思考就能正常工作,因为:

List<Car> cars;
List<Automobile> automobiles = cars;
// You'd expect this to work because Car is-a Automobile, but
// throws inconvertible types compile error.

错误的原因是正确的:List<Car> 没有 继承自List<Automobile>,因此它们之间不能相互赋值。只有泛型类型参数之间存在继承关系。你可能认为Java编译器无法充分理解你的情况,但是你可以通过给编译器提示来帮助它:

List<Car> cars;
List<? extends Automobile> automobiles = cars;   // no error

逆变性

逆变性是协变性的反面。在协变性中,参数类型必须具有子类型关系,而在逆变性中,它们必须具有超类型关系。这可以被视为继承的上界:任何超类型都允许向上包括指定的类型:

class AutoColorComparer implements Comparator<Automobile>
    public int compare(Automobile a, Automobile b) {
        // Return comparison of colors
    }

这可以与Collections.sort一起使用:

public static <T> void sort(List<T> list, Comparator<? super T> c)

// Which you can call like this, without errors:
List<Car> cars = getListFromSomewhere();
Collections.sort(cars, new AutoColorComparer());

你甚至可以使用比较器来调用它,并将其用于任何类型。

何时使用逆变或协变?

也许有点离题,但这有助于理解并回答你的问题。一般来说,当你获取东西时,请使用协变;当你放置东西时,请使用逆变。这在Stack Overflow问题的答案 How would contravariance be used in Java generics?中最好地解释了。

那么使用List<? extends Map<String, String>>是什么意思?

你使用了extends,因此应用了协变规则。这里有一个映射列表,你存储在列表中的每个项目必须是一个Map<string, string>或派生自它。语句List<Map<String, String>>不能派生自Map,但必须一个Map

因此,以下内容将有效,因为TreeMap继承自Map

List<Map<String, String>> mapList = new ArrayList<Map<String, String>>();
mapList.add(new TreeMap<String, String>());

但这样做不会:

List<? extends Map<String, String>> mapList = new ArrayList<? extends Map<String, String>>();
mapList.add(new TreeMap<String, String>());

这种方式也行不通,因为它并不满足协变约束条件:

List<? extends Map<String, String>> mapList = new ArrayList<? extends Map<String, String>>();
mapList.add(new ArrayList<String>());   // This is NOT allowed, List does not implement Map

还有什么需要注意的?

这可能很明显,但您可能已经注意到,使用extends关键字仅适用于该参数,而不适用于其余部分。也就是说,以下内容将无法编译:

List<? extends Map<String, String>> mapList = new List<? extends Map<String, String>>();
mapList.add(new TreeMap<String, Element>())  // This is NOT allowed

假设你想在地图中允许任何类型,键为字符串,你可以在每个类型参数上使用extend。例如,假设你处理XML并希望在地图中存储AttrNode、Element等,可以这样做:

List<? extends Map<String, ? extends Node>> listOfMapsOfNodes = new...;

// Now you can do:
listOfMapsOfNodes.add(new TreeMap<Sting, Element>());
listOfMapsOfNodes.add(new TreeMap<Sting, CDATASection>());

在"So what is it then with..."中的任何内容都无法编译。 - NobleUplift
@NobleUplift:如果您不提供错误消息,那么帮助您可能会很困难。作为替代方案,考虑在SO上提出新问题以获得答案,成功的机会更大。上面的代码只是片段,它取决于您在您的情况下如何实现它。 - Abel
我没有新问题,只有改进。List<? extends Map<String, String>> mapList = new ArrayList<? extends Map<String, String>>(); mapList.add(new TreeMap<String, String>()); 的结果是 found: ? extends java.util.Map<java.lang.String,java.lang.String> required: class or interface without boundsList<Map<String, String>> mapList = new ArrayList<Map<String, String>>(); mapList.add(new TreeMap<String, String>()); 则完美运行。显然,最后一个例子是正确的。 - NobleUplift
@NobleUplift:抱歉,我在旅途中,回来后会修复,感谢您指出这个明显的错误! :) - Abel
没问题,我很高兴它会被修复。如果你想批准,我已经为你做了编辑。 - NobleUplift

4
今天,我使用了这个功能,这里是我的一个非常新鲜的实际例子。(我已经将类和方法名称更改为通用名称,以便不分散注意力。)
我有一个方法,旨在接受一组A对象,我最初编写的签名如下:
void myMethod(Set<A> set)

但是我希望它可以使用 A 的子类的 Set 来调用它。但这是不允许的!(原因是,myMethod 可以向 set 添加类型为 A 而不是调用方所声明的子类型的对象。如果可能的话,它会破坏类型系统。)现在,泛型来解救了它,因为如果我改用这个方法签名,它就能按预期工作:
<T extends A> void myMethod(Set<T> set)

更简洁的做法是,在方法体中不需要使用实际类型时:
void myMethod(Set<? extends A> set)

这样,set的类型变成了A实际子类型的对象的集合,因此可以在不危及类型系统的情况下与子类一起使用。

0

正如您所提到的,定义List有以下两个版本:

  1. List<? extends Map<String, String>>
  2. List<?>

第二种方法非常开放。它可以容纳任何对象类型。但如果您想要一个给定类型的映射,则可能没有用处。例如,如果有人不小心放入了不同类型的映射,例如Map<String,int>,则您的使用方法可能会出错。

为了确保List可以容纳给定类型的对象,Java泛型引入了? extends。因此,在#1中,List可以容纳从Map<String,String>类型派生的任何对象。添加任何其他类型的数据都会抛出异常。


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