任何简单的方法来解释为什么我不能做的List <Animal> animals = new ArrayList <Dog>()?

我知道为什么不应该这样做。 但有没有办法向外行解释为什么这是不可能的。 你可以很容易地向外行解释: Animal animal = new Dog(); 。 狗是一种动物,但是狗的名单不是动物的名单。

想象一下,你创build一个列表。 然后,您将其声明为List <Animal>并将其交给同事。 他不是无理地相信他可以把放进去。

然后他把它交给你,现在你有一个的名单,在它的中间有一只 。 混沌随之而来。

需要注意的是,这个限制是由于列表的可变性造成的。 在斯卡拉(例如),你可以声明一个列表是一个动物列表。 这是因为Scala列表(默认情况下)是不可变的,所以将一个Cat添加到Dog列表中会给你一个动物列表。

你正在寻找的答案是关于协方差和逆变的概念。 有些语言支持这些(例如,.NET 4增加了支持),但是一些基本的问题通过这样的代码来演示:

 List<Animal> animals = new List<Dog>(); animals.Add(myDog); // works fine - this is a list of Dogs animals.Add(myCat); // would compile fine if this were allowed, but would crash! 

因为Cat会从动物中派生出来,所以编译时检查会提示它可以被添加到List中。 但是,在运行时,您不能将猫添加到狗的列表!

所以,虽然看起来很直观简单,但这些问题实际上是非常复杂的。

这里有一个在.NET 4中的co / contravariance的MSDN概述: http : //msdn.microsoft.com/en-us/library/dd799517 (VS.100) .aspx – 这也都适用于Java也是,虽然我不'不知道Java的支持是什么样的。

我能给出的最好的外行答案是这样的: 因为在devisegenerics时,他们不想重复对Java的数组types系统做出的不安全决定

这是可能的数组:

 Object[] objArray = new String[] { "Hello!" }; objArray[0] = new Object(); 

这段代码编译得很好,因为数组的types系统在Java中工作的方式。 它会在运行时引发一个ArrayStoreException

决定不允许这种不安全的行为的仿制药。

另请参见: Java Arrays Break Type Safety ,其中许多认为是Javadevise缺陷之一 。

你想要做的是以下几点:

 List<? extends Animal> animals = new ArrayList<Dog>() 

这应该工作。

列表<动物>是一个对象,您可以插入任何动物,例如猫或章鱼。 ArrayList <Dog>不是。

假设你可以做到这一点。 其中一个交给List<Animal>人合理地期望能够做的事情之一就是给它添加一个Giraffe 。 当有人试图给animals添加Giraffe时会发生什么? 运行时错误? 这似乎打破了编译时input的目的。

我想说最简单的答案是忽略猫狗,它们是不相关的。 列表本身很重要。

 List<Dog> 

 List<Animal> 

是不同的types,狗来源于动物根本没有关系。

这个声明是无效的

 List<Animal> dogs = new List<Dog>(); 

出于同样的原因,这是一个

 AnimalList dogs = new DogList(); 

而狗可能inheritance动物,由生成的列表类

 List<Animal> 

不会从所生成的列表类inheritance

 List<Dog> 

这是一个错误的假设,因为两个类是相关的,使用它们作为通用参数将使那些generics类也是相关的。 虽然你当然可以添加一只狗到一个

 List<Animal> 

这并不意味着这一点

 List<Dog> 

是的一个子类

 List<Animal> 

注意,如果你有

 List<Dog> dogs = new ArrayList<Dog>() 

那么,如果你能做到的话

 List<Animal> animals = dogs; 

这不会把dogs变成一个List<Animal> 。 动物的底层数据结构仍然是一个ArrayList<Dog> ,所以如果你尝试将一个Elephant插入到animals ,实际上是将它插入到一个ArrayList<Dog> ,这是不起作用的(大象显然太大了; – )。

首先,我们来定义我们的动物王国:

 interface Animal { } class Dog implements Animal{ Integer dogTag() { return 0; } } class Doberman extends Dog { } 

考虑两个参数化接口:

 interface Container<T> { T get(); } interface Comparator<T> { int compare(T a, T b); } 

这些TDog

 class DogContainer implements Container<Dog> { private Dog dog; public Dog get() { dog = new Dog(); return dog; } } class DogComparator implements Comparator<Dog> { public int compare(Dog a, Dog b) { return a.dogTag().compareTo(b.dogTag()); } } 

你在这个Container接口的上下文中提出的是非常合理的:

 Container<Dog> kennel = new DogContainer(); // Invalid Java because of invariance. // Container<Animal> zoo = new DogContainer(); // But we can annotate the type argument in the type of zoo to make // to make it co-variant. Container<? extends Animal> zoo = new DogContainer(); 

那么为什么Java不会自动执行此操作? 考虑这对Comparator意味着什么。

 Comparator<Dog> dogComp = new DogComparator(); // Invalid Java, and nonsensical -- we couldn't use our DogComparator to compare cats! // Comparator<Animal> animalComp = new DogComparator(); // Invalid Java, because Comparator is invariant in T // Comparator<Doberman> dobermanComp = new DogComparator(); // So we introduce a contra-variance annotation on the type of dobermanComp. Comparator<? super Doberman> dobermanComp = new DogComparator(); 

如果Java自动允许将Container<Dog>分配给Container<Animal> ,那么人们也会期望Comparator<Dog>可以被分配给Comparator<Animal> ,这是没有意义的 – Comparator<Dog>比较两只猫?

ContainerComparator什么区别呢? 容器产生typesT值,而Comparator 消耗它们。 这些对应于types参数的协变反变化的用法。

有时types参数被用在两个位置,使得接口不变

 interface Adder<T> { T plus(T a, T b); } Adder<Integer> addInt = new Adder<Integer>() { public Integer plus(Integer a, Integer b) { return a + b; } }; Adder<? extends Object> aObj = addInt; // Obscure compile error, because it there Adder is not usable // unless T is invariant. //aObj.plus(new Object(), new Object()); 

为了向后兼容,Java默认为不变 。 你必须明确地select适当的方差? extends X ? extends X? super X ? super X关于variables的types,字段,参数或方法的返回值。

这是一个真正的麻烦 – 每次有人使用一个generics,他们必须做出这个决定! ContainerComparator的作者当然应该能够一劳永逸地声明这一点。

这被称为“声明网站差异”,并在Scala中提供。

 trait Container[+T] { ... } trait Comparator[-T] { ... } 

如果你不能改变列表,那么你的推理就会完美无缺。 不幸的是,一个List<>是必要的操纵。 这意味着你可以通过添加一个新的Animal来改变一个List<Animal> 。 如果允许使用List<Dog>作为List<Animal> ,则可以列出包含Cat的列表。

如果List<>不能突变(就像在Scala中一样),那么你可以把A List<Dog>当作List<Animal> 。 例如,C#使协变和逆变的genericstypes参数成为可能。

这是更一般的Liskov替代主体的一个例子。

突变引起你一个问题的事实发生在别处。 考虑typesSquareRectangle

Square是一个Rectangle ? 当然 – 从math的angular度来看。

你可以定义一个Rectangle类,它提供可读的getWidthgetHeight属性。

您甚至可以根据这些属性添加计算其areaperimeter方法。

然后,您可以定义一个Square类,该类的子类为Rectangle ,并使getWidthgetHeight返回相同的值。

但是当你通过setWidthsetHeight开始允许突变时会发生什么?

现在, Square不再是Rectangle的合理子类。 突变其中一个属性将不得不默默地改变另一个以保持不变,而Liskov的替代主体将会被违反。 改变Square的宽度会产生意想不到的副作用。 为了保持一个正方形,你将不得不改变高度,但你只是要求改变宽度!

只要可以使用Rectangle就不能使用Square 。 所以, 在突变的情况下, Square不是Rectangle

你可以在Rectangle上创build一个新的方法,它知道如何克隆一个新的宽度或者一个新的高度,然后你的Square可以在克隆过程中安全地转移到一个Rectangle ,但是现在你不再改变原始值了。

同样,当一个List<Dog>的接口允许你添加新的项目到列表中时,它不能是一个List<Animal>

这是因为generics是不变的 。

英语答案:

如果List<Dog>List<Animal> ,则前者必须支持(inheritance)后者的所有操作。 添加一只猫可以做到后者,但不是以前。 所以这个'是'的关系失败了。

编程答案:

types安全

一个保守的语言默认deviseselect,停止这种腐败:

 List<Dog> dogs = new List<>(); dogs.add(new Dog("mutley")); List<Animal> animals = dogs; animals.add(new Cat("felix")); // Yikes!! animals and dogs refer to same object. dogs now contains a cat!! 

为了build立子types关系,必须对“可铸性”/“可替代性”标准进行分类。

  1. 法律对象替代 – 对祖先支持的所有操作:

     // Legal - one object, two references (cast to different type) Dog dog = new Dog(); Animal animal = dog; 
  2. 替代法律 – 所有祖先支持的后代操作:

     // Legal - one object, two references (cast to different type) List<Animal> list = new List<Animal>() Collection<Animal> coll = list; 
  3. 非法的通用replace(types参数的转换) – 不受支持的操作符:

     // Illegal - one object, two references (cast to different type), but not typesafe List<Dog> dogs = new List<Dog>() List<Animal> animals = list; // would-be ancestor has broader ops than decendant 

然而

根据generics类的devise,types参数可以在“安全位置”中使用,这意味着铸造/replace有时可以成功而不会破坏types安全性。 协方差意味着如果U是G<T>的相同types或者子types,那么通用不定向G<U>可以代替G<T> T。反向意味着通用实例G<U>可以代替G<T>如果U是G<T>的相同types或超types。这两个例子是安全的:

  • 协变立场:

    • 方法返回types (genericstypes的输出) – 子types必须相同/更具限制性,所以它们的返回types符合祖先
    • 不可变字段的types (由owner类设置,然后是“仅在内部输出”) – 子types必须更具限制性,所以当它们设置不可变字段时,它们符合祖先

    在这种情况下,可以使用像这样的后代来replacetypes参数:

     SomeCovariantType<Dog> decendant = new SomeCovariantType<>; SomeCovariantType<? extends Animal> ancestor = decendant; 

    通配符加'延伸'给出了使用站点指定的协方差。

  • 控制位置:

    • 方法参数types (input到genericstypes) – 子types必须相同/更加适应,以便在传递祖先参数时不会中断
    • 上层types参数边界 (内部types实例化) – 子types必须相同/更加适应,所以当祖先设置variables值时它们不会中断

    在这些情况下,允许用这样的祖先来replacetypes参数是可以安全的:

     SomeContravariantType<Animal> decendant = new SomeContravariantType<>; SomeContravariantType<? super Dog> ancestor = decendant; 

    通配符加'超级'给出了使用站点指定的逆转。

使用这两个习语需要开发者付出额外的努力和关注才能获得“可替代性的力量”。 Java需要手动开发人员的努力来确保types参数分别真正用于协变/逆变位置(因此types安全)。 我不知道为什么 – 例如scala编译器检查: – /。 你基本上是告诉编译器'相信我,我知道我在做什么,这是types安全的'。

  • 不变的职位

    • 可变字段types (内部input和输出) – 可以被所有的祖先和子types读写 – 读是协变的,写是逆变的; 结果是不变的
    • (如果在协变和逆变位置都使用types参数,那么这将导致不变)

通过inheritance你实际上是创build几个类的通用types。 在这里你有一个共同的动物types。 你正在使用它创build一个动物types的数组,并保持类似types的值(inheritancetypes的狗,猫等)。

例如:

  dim animalobj as new List(Animal) animalobj(0)=new dog() animalobj(1)=new Cat() 

…….

得到它了?