【发布时间】:2011-01-21 18:01:39
【问题描述】:
我知道为什么不应该这样做。但是有没有办法向外行解释为什么这是不可能的。您可以轻松地向外行解释:Animal animal = new Dog();。狗是一种动物,但狗的列表不是动物的列表。
【问题讨论】:
标签: java generics oop covariance
我知道为什么不应该这样做。但是有没有办法向外行解释为什么这是不可能的。您可以轻松地向外行解释:Animal animal = new Dog();。狗是一种动物,但狗的列表不是动物的列表。
【问题讨论】:
标签: java generics oop covariance
List
【讨论】:
假设你可以这样做。有人提交了List<Animal> 合理地期望能够做的事情之一就是向其添加Giraffe。当有人尝试将Giraffe 添加到animals 时会发生什么?运行时错误?这似乎违背了编译时类型的目的。
【讨论】:
Giraffe,为什么会出现运行时错误?如果我这样做animals.get() 我只能期待animal 而Giraffe 是animal。在ArrayList<Dog>() 中添加Giraffe 会很奇怪,但我没有看到任何运行时错误。所有类型都在运行时被擦除。
ArrayList<Dog>,那将是正确的。如果没有,一切都会好起来的,对吧?
请注意,如果您有
List<Dog> dogs = new ArrayList<Dog>()
如果可以的话
List<Animal> animals = dogs;
这不会将dogs 变成List<Animal>。动物底层的数据结构仍然是ArrayList<Dog>,所以如果你尝试将Elephant插入animals,你实际上是在将它插入ArrayList<Dog>,这是行不通的(大象显然也是如此)大;-)。
【讨论】:
ArrayList<Dog> 引用,那将是正确的。如果没有,一切都会好起来的,对吧?
通过继承,您实际上是在为多个类创建通用类型。这里有一个常见的动物类型。您通过创建一个 Animal 类型的数组并保留相似类型的值(继承类型 dog、cat 等)来使用它。
例如:
dim animalobj as new List(Animal)
animalobj(0)=new dog()
animalobj(1)=new Cat()
.......
知道了吗?
【讨论】:
假设您创建了一个狗列表。然后,您将其声明为 List
然后他把它还给你,你现在有一个狗列表,中间有一只猫。混乱随之而来。
请务必注意,由于列表的可变性,存在此限制。在 Scala 中(例如),您可以声明 Dogs 列表是 Animals 列表。这是因为 Scala 列表(默认情况下)是不可变的,因此将 Cat 添加到 Dogs 列表中将为您提供 新 列表strong>动物。
【讨论】:
您正在寻找的答案与称为协变和逆变的概念有关。一些语言支持这些(例如 .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 的支持是什么样的。
【讨论】:
您正在尝试执行以下操作:
List<? extends Animal> animals = new ArrayList<Dog>()
应该可以的。
【讨论】:
这是因为泛型类型是invariant。
【讨论】:
首先,让我们定义我们的动物王国:
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 有什么区别?容器产生 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 默认为不变性。您必须在变量、字段、参数或方法返回的类型上使用? extends X 或? super X 显式选择适当的差异。
这真的很麻烦——每次有人使用泛型类型时,他们都必须做出这个决定!当然Container 和Comparator 的作者应该能够一劳永逸地声明这一点。
这称为“声明站点差异”,可在 Scala 中使用。
trait Container[+T] { ... }
trait Comparator[-T] { ... }
【讨论】:
我能给出的最好的外行回答是:因为在设计泛型时,他们不想重复对 Java 的数组类型系统做出的相同决定,使其不安全。
这可以通过数组实现:
Object[] objArray = new String[] { "Hello!" };
objArray[0] = new Object();
这段代码编译得很好,因为数组的类型系统在 Java 中的工作方式。它会在运行时引发ArrayStoreException。
决定不允许泛型出现这种不安全的行为。
另见其他地方:Java Arrays Break Type Safety,许多人认为它是Java Design Flaws 之一。
【讨论】:
如果您无法更改列表,那么您的推理将非常合理。不幸的是,List<> 被强制操作。这意味着您可以通过添加新的Animal 来更改List<Animal>。如果您被允许使用List<Dog> 作为List<Animal>,您最终可能会得到一个还包含Cat 的列表。
如果List<> 不能突变(就像在 Scala 中一样),那么您可以将 A List<Dog> 视为 List<Animal>。例如,C# 使用协变和逆变泛型类型参数使这种行为成为可能。
这是更一般的Liskov substitution principal 的一个实例。
突变导致您出现问题的事实发生在其他地方。考虑类型Square 和Rectangle。
Square 是 Rectangle 吗?当然——从数学的角度来看。
您可以定义一个Rectangle 类,它提供可读的getWidth 和getHeight 属性。
您甚至可以根据这些属性添加计算其area 或perimeter 的方法。
然后您可以定义一个 Square 类,它是 Rectangle 的子类,并使 getWidth 和 getHeight 返回相同的值。
但是当您开始通过setWidth 或setHeight 允许突变时会发生什么?
现在,Square 不再是 Rectangle 的合理子类。改变其中一个属性将不得不默默地改变另一个以保持不变性,并且将违反 Liskov 的替换原则。更改 Square 的宽度会产生意想不到的副作用。为了保持正方形,您还必须更改高度,但您只要求更改宽度!
当您可以使用Rectangle 时,您不能使用您的Square。因此,存在突变Square 不是Rectangle!
您可以在Rectangle 上创建一个新方法,该方法知道如何克隆具有新宽度或新高度的矩形,然后您的Square 可以在克隆过程中安全地转移到Rectangle,但是现在您不再改变原始值。
当List<Dog> 的界面允许您向列表中添加新项目时,List<Dog> 也不能是List<Animal>。
【讨论】:
我想说最简单的答案是忽略猫和狗,它们无关紧要。重要的是列表本身。
List<Dog>
和
List<Animal>
是不同的类型,Dog 派生自 Animal 与此无关。
此声明无效
List<Animal> dogs = new List<Dog>();
出于同样的原因
AnimalList dogs = new DogList();
虽然 Dog 可能继承自 Animal,但由
生成的列表类List<Animal>
不继承自
生成的列表类List<Dog>
假设因为两个类是相关的,所以将它们用作泛型参数会使这些泛型类也相关是错误的。虽然您当然可以将狗添加到
List<Animal>
这并不意味着
List<Dog>
是
的子类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
根据泛型类的设计,类型参数可以在“安全位置”使用,这意味着转换/替换有时可以成功而不会破坏类型安全。协变意味着泛型实例 G<U> 可以替换 G<T> 如果 U 是 T 的相同类型或子类型。逆变意味着泛型实例 G<U> 可以替换 G<T> 如果 U 是相同类型或 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编译器检查这个:-/。你基本上是在告诉编译器“相信我,我知道我在做什么,这是类型安全的”。
不变的位置
【讨论】: