Any simple way to explain why I cannot do List<Animal> animals = new ArrayList<Dog>()?
我知道为什么人们不应该这样做。但是有没有办法向外行解释为什么这是不可能的呢?你可以很容易地向外行解释:
想象你创建了一个狗的列表。然后您将此声明为list
然后他把它还给你,你现在有了一张狗的清单,其中有一只猫。随之而来的是混乱。
需要注意的是,由于列表的可变性,存在这种限制。在scala中(例如),您可以声明狗的列表是动物的列表。这是因为scala列表(默认情况下)是不可变的,所以在狗列表中添加一只猫可以为您提供一个新的动物列表。
我能给出的最好的外行回答是:因为在设计泛型时,他们不想重复对Java的数组类型系统做出的不安全的决定。
这在数组中是可能的:
由于数组的类型系统在Java中工作,所以这个代码编译得很好。它将在运行时提升一个
决定不允许非专利药品出现这种不安全行为。
其他地方也可以看到:Java数组破坏类型安全性,许多人认为Java设计缺陷之一。
你要寻找的答案是关于协方差和反方差的概念。有些语言支持这些功能(例如,NET 4增加了支持),但是一些基本问题可以通过以下代码来演示:
1 2 3 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! |
因为猫是从动物身上衍生出来的,所以编译时的检查会建议把它添加到列表中。但是,在运行时,不能将猫添加到狗列表中!
所以,虽然看起来很简单,但这些问题实际上是非常复杂的。
在.NET 4中有一个MS/DN的概述:http://MSDN,微软.com /Eng/Engult/Du79517(VS.100).ASPX -它都适用于Java,虽然我不知道Java的支持是什么样的。
您要做的是:
1 | List<? extends Animal> animals = new ArrayList<Dog>() |
那应该管用。
list
我想说最简单的答案是忽略猫和狗,它们不相关。重要的是列表本身。
1 | List<Dog> |
和
1 | List<Animal> |
是不同的类型,狗源于动物,与此毫无关系。
此语句无效
1 | List<Animal> dogs = new List<Dog>(); |
因为同样的原因
1 | AnimalList dogs = new DogList(); |
虽然狗可以从动物继承,但列表类由
1 | List<Animal> |
不从由生成的列表类继承
1 | List<Dog> |
假设两个类是相关的,将它们用作泛型参数将使这些泛型类也相关,这是错误的。当然你也可以在
1 | List<Animal> |
这并不意味着
1 | List<Dog> |
是的子类
1 | List<Animal> |
假设你能做到。有人把一个
如果你不能改变列表,那么你的推理就完全正确了。不幸的是,
如果
这是更一般的李斯科夫代换原理的一个例子。
事实上,突变导致你在这里的问题发生在其他地方。考虑类型
您可以定义一个提供可读的
甚至可以添加基于这些属性计算其
然后,可以定义一个
但是当你开始允许通过
现在,
你不能在任何时候使用你的
你可以在
同样,当
首先,让我们定义一下我们的动物王国:
1 2 3 4 5 6 7 8 9 10 11 | interface Animal { } class Dog implements Animal{ Integer dogTag() { return 0; } } class Doberman extends Dog { } |
考虑两个参数化接口:
1 2 3 4 5 6 7 | interface Container<T> { T get(); } interface Comparator<T> { int compare(T a, T b); } |
其中
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | 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()); } } |
在这个
1 2 3 4 5 6 7 8 | 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不自动执行这个操作呢?考虑一下这对
1 2 3 4 5 6 7 8 9 10 | 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自动允许EDOCX1 14的分配被分配给EDCOX1(15),人们也会期望EDCOX1×16可能被分配给EDCOX1×17,这是没有意义的——EDCOX1与16如何比较两个猫?
那么,
有时在两个位置都使用类型参数,使得接口不变。
1 2 3 4 5 6 7 8 9 10 11 12 13 | 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默认为不变性。在变量、字段、参数或方法返回的类型上,必须明确选择与
这是一个真正的难题——每当有人使用通用类型时,他们必须做出这个决定!当然,
这称为"声明站点差异",在scala中可用。
1 2 |
注意如果你有
1 | List<Dog> dogs = new ArrayList<Dog>() |
那么,如果你能做到的话
1 | List<Animal> animals = dogs; |
这并不能使
这是因为泛型类型是不变的。
英语回答:
如果"
编程答案:
类型安全一个保守的语言默认设计选项,用于阻止此损坏:
1 2 3 4 5 | 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!! |
为了建立子类型关系,必须使用"可转换性/子适配性"标准。
合法对象子状态-对decendant支持的祖先执行的所有操作:
1 2 3 | // Legal - one object, two references (cast to different type) Dog dog = new Dog(); Animal animal = dog; |
合法集合替换-对后代支持的祖先的所有操作:
1 2 3 | // Legal - one object, two references (cast to different type) List<Animal> list = new List<Animal>() Collection<Animal> coll = list; |
非法的泛型替换(类型参数的强制转换)-decendant中不支持的操作:
1 2 3 | // 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 |
然而
根据泛型类的设计,类型参数可用于"安全位置",这意味着有时可以成功地进行强制转换/替换,而不会破坏类型安全。协方差是指当u是t的同一类型或子类型时,
协变位置:
- 方法返回类型(泛型类型的输出)-子类型必须具有相同/更严格的限制,因此它们的返回类型符合祖先类型
- 不可变字段的类型(由所有者类设置,然后是"仅内部输出")—子类型必须更严格,因此当它们设置不可变字段时,它们符合祖先
在这些情况下,允许具有如下冗余的类型参数的可替换性是安全的:
1
2SomeCovariantType<Dog> decendant = new SomeCovariantType<>;
SomeCovariantType<? extends Animal> ancestor = decendant;通配符加上"extends"提供使用站点指定的协方差。
控制变量位置:
- 方法参数类型(输入到泛型类型)-子类型必须具有相同/更大的适应能力,以便在传递祖先的参数时不会中断
- 类型参数上限(内部类型实例化)-子类型必须具有相同的/更大的适应能力,以便在祖先设置变量值时不会中断。
在这些情况下,允许具有如下祖先的类型参数的可替换性是安全的:
1
2SomeContravariantType<Animal> decendant = new SomeContravariantType<>;
SomeContravariantType<? super Dog> ancestor = decendant;通配符加上"super"给出了使用站点指定的反差。
使用这两个习语需要开发人员额外的努力和注意,以获得"可替换性能力"。Java需要手动开发人员努力确保类型参数在共变/逆变位中真正使用(因此类型安全)。我不知道为什么-例如scala编译器检查这个:-/。你基本上是在告诉编译器"相信我,我知道我在做什么,这是类型安全的"。
不变位置
- 可变字段的类型(内部输入和输出)-可以由所有祖先和子类读写-读是协变的,写是逆变的;结果是不变的
- (同样,如果类型参数同时用于协变和逆变位置,则会导致不变性)
通过继承,实际上您正在为几个类创建公共类型。这里有一种常见的动物类型。您可以通过在动物类型中创建一个数组并保留类似类型的值(继承类型:狗、猫等)来使用它。
如:
1 2 3 |
……
知道了?