List < Map < String,String > > vs List <

这两者之间有什么区别吗

List<Map<String, String>>

还有

List<? extends Map<String, String>>

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

137245 次浏览

不能将 List<NavigableMap<String,String>>等类型的表达式分配给第一个。

(如果你想知道为什么你不能把 List<String>分配给 List<Object>,请看 数不胜数关于 SO 的其他问题。)

区别在于,例如,一个

List<HashMap<String,String>>

是一个

List<? extends Map<String,String>>

但不是

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
}

你可能认为 HashMapList应该是 MapList,但是有一个很好的理由为什么它不是:

假设你可以这样做:

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

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

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

2号非常开放。它可以容纳任何对象类型。如果您想要一个给定类型的映射,这可能没有用。以防有人不小心放入不同类型的地图,例如,Map<String, int>。您的消费者方法可能会中断。

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

今天,我已经使用了这个特性,所以这里是我非常新鲜的实际例子。(我已经将类和方法的名称更改为泛型名称,这样它们就不会偏离实际要点。)

我有一个方法,用来接受 A对象的 Set,这是我最初用这个签名写的:

void myMethod(Set<A> set)

但是它实际上想用 A的子类的 Set来调用它。但这是不允许的!(原因是,myMethod可以向 set添加属于 A类型的对象,但不属于 set的对象声明为位于调用者站点的子类型。因此,如果可能的话,这可能会破坏类型系统。)

现在来看一下泛型,因为如果我使用这个方法签名,它就会按预期的方式工作:

<T extends A> void myMethod(Set<T> set)

或者更短,如果你不需要在方法主体中使用实际的类型:

void myMethod(Set<? extends A> set)

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

我在其他答案中缺少的是一个参考,它是如何与协变和逆变以及子类型和超类型(即多态性) ,特别是与 Java 相关联的。OP 也许能够很好地理解这一点,但为了以防万一,这样说吧:

协方差

如果你有一个类 Automobile,那么 CarTruck是它们的子类型。任何汽车都可以被赋予一个类型为 Automobile 的变量,这在面向对象中是众所周知的,称为多态性。协方差是指在具有泛型或委托的场景中使用相同的原则。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
}

这可以与 收藏品,分类一起使用:

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 问题的回答 < em > 如何在 Java 泛型中使用逆变? 中得到了最好的解释。

那么 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>());