任何简单的方法来解释为什么我不能做的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); }
这些T
是Dog
。
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>
比较两只猫?
那Container
和Comparator
什么区别呢? 容器产生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,他们必须做出这个决定! Container
和Comparator
的作者当然应该能够一劳永逸地声明这一点。
这被称为“声明网站差异”,并在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替代主体的一个例子。
突变引起你一个问题的事实发生在别处。 考虑typesSquare
和Rectangle
。
Square
是一个Rectangle
? 当然 – 从math的angular度来看。
你可以定义一个Rectangle
类,它提供可读的getWidth
和getHeight
属性。
您甚至可以根据这些属性添加计算其area
或perimeter
方法。
然后,您可以定义一个Square
类,该类的子类为Rectangle
,并使getWidth
和getHeight
返回相同的值。
但是当你通过setWidth
或setHeight
开始允许突变时会发生什么?
现在, 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关系,必须对“可铸性”/“可替代性”标准进行分类。
-
法律对象替代 – 对祖先支持的所有操作:
// Legal - one object, two references (cast to different type) Dog dog = new Dog(); Animal animal = dog;
-
替代法律 – 所有祖先支持的后代操作:
// Legal - one object, two references (cast to different type) List<Animal> list = new List<Animal>() Collection<Animal> coll = list;
-
非法的通用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()
…….
得到它了?