我知道为什么不应该这样做。但是有没有办法向外行解释为什么这是不可能的。你可以很容易地向外行解释这个:Animal animal = new Dog();
。狗是一种动物,但狗的列表不是动物的列表。
13 回答
想象一下,您创建了一个Dogs列表。然后,您将其声明为List<Animal>并将其交给同事。他不无道理地相信他可以把一只猫放进去。
然后他把它还给你,你现在有一个Dogs列表,中间有一只Cat。混乱随之而来。
重要的是要注意,由于列表的可变性,存在此限制。在 Scala(例如)中,您可以声明Dogs列表是Animals列表。这是因为 Scala 列表(默认情况下)是不可变的,因此将Cat添加到Dogs列表会为您提供一个新的Animals列表。
您正在寻找的答案与称为协变和逆变的概念有关。一些语言支持这些(例如,.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 是从 animal 派生的,所以编译时检查会建议它可以添加到 List。但是,在运行时,您不能将 Cat 添加到 Dogs 列表中!
所以,尽管看起来很简单,但这些问题实际上是非常复杂的。
这里有一个关于 .NET 4 中协/逆变的 MSDN 概述:http: //msdn.microsoft.com/en-us/library/dd799517 (VS.100).aspx - 它也适用于 java,尽管我不知道不知道Java的支持是什么样的。
我能给出的最好的外行答案是:因为在设计泛型时,他们不想重复对 Java 的数组类型系统做出的相同决定,这使得它变得不安全。
这可以通过数组实现:
Object[] objArray = new String[] { "Hello!" };
objArray[0] = new Object();
由于数组的类型系统在 Java 中的工作方式,这段代码编译得很好。它会ArrayStoreException
在运行时引发。
决定不允许泛型出现这种不安全的行为。
另见其他地方:Java Arrays Break Type Safety,许多人认为这是Java Design Flaws之一。
List<Animal>是一个对象,您可以在其中插入任何动物,例如猫或章鱼。ArrayList <Dog>不是。
您正在尝试执行以下操作:
List<? extends Animal> animals = new ArrayList<Dog>()
那应该行得通。
我想说最简单的答案是忽略猫和狗,它们无关紧要。重要的是列表本身。
List<Dog>
和
List<Animal>
是不同的类型,从 Animal 派生的 Dog 与此无关。
该声明无效
List<Animal> dogs = new List<Dog>();
出于同样的原因,这个是
AnimalList dogs = new DogList();
虽然 Dog 可以从 Animal 继承,但生成的列表类由
List<Animal>
不从生成的列表类继承
List<Dog>
假设因为两个类是相关的,所以将它们用作泛型参数将使这些泛型类也相关是错误的。虽然您当然可以将狗添加到
List<Animal>
这并不意味着
List<Dog>
是一个子类
List<Animal>
假设你可以做到这一点。有人递给 aList<Animal>
会合理地期望能够做的事情之一就是在其中添加 a Giraffe
。当有人尝试添加Giraffe
to时会发生什么animals
?运行时错误?这似乎违背了编译时类型的目的。
请注意,如果您有
List<Dog> dogs = new ArrayList<Dog>()
那么,如果你能做到
List<Animal> animals = dogs;
这不会变成. dogs
_ List<Animal>
动物底层的数据结构仍然是 an ArrayList<Dog>
,因此如果您尝试将 an 插入Elephant
到animals
中,实际上是将其插入到 anArrayList<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);
}
以及这些 where T
is 的实现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>
,人们也会期望 aComparator<Dog>
可以分配给 a Comparator<Animal>
,这是没有意义的——a 怎么能Comparator<Dog>
比较两个 Cats?
那么Container
和 和有什么不一样Comparator
?Container产生type 的值T
,而Comparator
消费它们。这些对应于类型参数的协变和反变用法。
有时类型参数在两个位置都使用,使接口不变。
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 默认为invariance。您必须明确地选择与变量、字段、参数或方法返回的类型相关? extends X
的适当方差。? super X
这是一个真正的麻烦——每次有人使用泛型类型时,他们必须做出这个决定!Container
当然,作者Comparator
应该能够一劳永逸地宣布这一点。
这称为“声明站点差异”,可在 Scala 中使用。
trait Container[+T] { ... }
trait Comparator[-T] { ... }
如果您不能改变列表,那么您的推理将是完全合理的。不幸的是, a List<>
被强制操作。这意味着您可以List<Animal>
通过添加一个新Animal
的来更改它。如果您被允许将 aList<Dog>
用作 a ,List<Animal>
那么您最终可能会得到一个也包含 a 的列表Cat
。
如果List<>
不能突变(如在 Scala 中),那么您可以将 AList<Dog>
视为List<Animal>
. 例如,C# 使用协变和逆变泛型类型参数使这种行为成为可能。
这是更一般的Liskov 替换原则的一个实例。
突变导致您出现问题的事实发生在其他地方。考虑类型Square
和Rectangle
。
是Square
一个Rectangle
吗?当然——从数学的角度来看。
您可以定义一个Rectangle
提供可读性getWidth
和getHeight
属性的类。
您甚至可以添加基于这些属性计算其area
or的方法。perimeter
然后,您可以定义一个Square
子类Rectangle
并生成两者getWidth
并getHeight
返回相同值的类。
但是当您开始通过setWidth
or允许突变时会发生什么setHeight
?
现在,Square
不再是Rectangle
. 改变其中一个属性将不得不默默地改变另一个以保持不变性,并且将违反 Liskov 的替换原则。改变 a 的宽度Square
会产生意想不到的副作用。为了保持正方形,您还必须更改高度,但您只要求更改宽度!
你不能使用你的Square
,只要你可以使用Rectangle
. 因此,在存在突变的情况下aSquare
不是 a Rectangle
!
您可以创建一个Rectangle
知道如何克隆具有新宽度或新高度的矩形的新方法,然后您Square
可以Rectangle
在克隆过程中安全地转移到 a ,但现在您不再改变原始值。
同样,当它的界面允许您向列表中添加新项目时, aList<Dog>
不能是 a 。List<Animal>
这是因为泛型类型是不变的。
英文答案:
如果'List<Dog>
是List<Animal>
',则前者必须支持(继承)后者的所有操作。添加猫可以对后者进行,但不能对前者进行。所以“是”关系失败了。
编程答案:
类型安全
一种保守的语言默认设计选择,可以阻止这种损坏:
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!!
为了具有子类型关系,必须满足“可转换性”/“可替代性”标准。
合法对象替换 - 后代支持的祖先的所有操作:
// 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;
非法泛型替换(类型参数转换) - 后代中不支持的操作:
// 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 的相同类型或子类型,则泛型实例可以替换G<U>
。逆变意味着如果 U 是 T 的相同类型或超类型,则泛型实例可以替换。这些是 2 种情况的安全位置:G<T>
G<U>
G<T>
协变位置:
- 方法返回类型(泛型类型的输出) - 子类型必须同样/更具限制性,因此它们的返回类型符合祖先
- 不可变字段的类型(由所有者类设置,然后是“仅内部输出”) - 子类型必须更具限制性,因此当它们设置不可变字段时,它们符合祖先
在这些情况下,允许使用这样的后代替换类型参数是安全的:
SomeCovariantType<Dog> decendant = new SomeCovariantType<>; SomeCovariantType<? extends Animal> ancestor = decendant;
通配符加上'extends'给出了使用站点指定的协方差。
逆变位置:
- 方法参数类型(泛型类型的输入) - 子类型必须同等/更适应,以便在传递祖先的参数时它们不会中断
- 类型参数上限(内部类型实例化) - 子类型必须同等/更适应,因此当祖先设置变量值时它们不会中断
在这些情况下,允许使用这样的祖先替换类型参数是安全的:
SomeContravariantType<Animal> decendant = new SomeContravariantType<>; SomeContravariantType<? super Dog> ancestor = decendant;
通配符加上“super”给出了使用站点指定的逆变性。
使用这两个习语需要开发人员付出额外的努力和关注才能获得“替代能力”。Java 需要手动开发人员来确保类型参数分别真正用于协变/逆变位置(因此是类型安全的)。我不知道为什么 - 例如 scala 编译器会检查这个:-/。你基本上是在告诉编译器“相信我,我知道我在做什么,这是类型安全的”。
不变的位置
- 可变字段的类型(内部输入和输出) - 可以被所有祖先和子类型类读写 - 读取是协变的,写入是逆变的;结果是不变的
- (如果类型参数同时用于协变和逆变位置,则这会导致不变性)
通过继承,您实际上是在为多个类创建通用类型。这里有一个常见的动物类型。您通过创建一个 Animal 类型的数组并保留相似类型的值(继承类型 dog、cat 等)来使用它。
例如:
dim animalobj as new List(Animal)
animalobj(0)=new dog()
animalobj(1)=new Cat()
…………
知道了?