我在其他答案中缺少的是对这与一般的协变和逆变以及子类型和超类型(即多态性)以及特别是 Java 的关系的参考。 OP 可能很好理解这一点,但以防万一,这里是这样的:
协方差
如果你有一个类Automobile,那么Car 和Truck 是它们的子类型。任何 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());
您甚至可以使用比较对象的比较器调用它,并将其与任何类型一起使用。
何时使用反方差或协方差?
也许有点过时,你没有问,但它有助于理解回答你的问题。一般来说,当你得到东西时,使用协变,当你放东西时,使用逆变。这在an answer to Stack Overflow question How would contravariance be used in Java generics? 中得到了最好的解释。
那么List<? extends Map<String, String>> 又是什么呢?
您使用extends,因此适用于协方差的规则。在这里,您有一个地图列表,并且您存储在列表中的每个项目都必须是 Map<string, string> 或派生自它。语句List<Map<String, String>> 不能派生自Map,而必须是a 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>());