22

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

4

13 回答 13

49

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

然后他把它还给你,你现在有一个Dogs列表,中间有一只Cat。混乱随之而来。

重要的是要注意,由于列表的可变性,存在此限制。在 Scala(例如)中,您可以声明Dogs列表是Animals列表。这是因为 Scala 列表(默认情况下)是不可变的,因此将Cat添加到Dogs列表会为您提供一个新的Animals列表。

于 2010-02-27T09:38:48.907 回答
8

您正在寻找的答案与称为协变和逆变的概念有关。一些语言支持这些(例如,.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的支持是什么样的。

于 2010-02-27T09:40:37.453 回答
8

我能给出的最好的外行答案是:因为在设计泛型时,他们不想重复对 Java 的数组类型系统做出的相同决定,这使得它变得不安全

这可以通过数组实现:

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

由于数组的类型系统在 Java 中的工作方式,这段代码编译得很好。它会ArrayStoreException在运行时引发。

决定不允许泛型出现这种不安全的行为。

另见其他地方:Java Arrays Break Type Safety,许多人认为这是Java Design Flaws之一。

于 2010-02-27T13:03:20.237 回答
5

List<Animal>是一个对象,您可以在其中插入任何动物,例如猫或章鱼。ArrayList <Dog>不是。

于 2010-02-27T09:16:16.640 回答
5

您正在尝试执行以下操作:

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

那应该行得通。

于 2010-02-27T09:44:31.890 回答
4

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

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>
于 2010-03-03T03:29:37.067 回答
3

假设你可以做到这一点。有人递给 aList<Animal>会合理地期望能够做的事情之一就是在其中添加 a Giraffe。当有人尝试添加Giraffeto时会发生什么animals?运行时错误?这似乎违背了编译时类型的目的。

于 2010-02-27T09:18:17.390 回答
2

请注意,如果您有

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

那么,如果你能做到

List<Animal> animals = dogs;

不会变成. dogs_ List<Animal>动物底层的数据结构仍然是 an ArrayList<Dog>,因此如果您尝试将 an 插入Elephantanimals中,实际上是将其插入到 anArrayList<Dog>中不起作用(大象显然太大了;-)。

于 2010-02-27T09:30:44.057 回答
2

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

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 Tis 的实现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] { ... }
于 2010-02-27T11:57:33.623 回答
2

如果您不能改变列表,那么您的推理将是完全合理的。不幸的是, a List<>被强制操作。这意味着您可以List<Animal>通过添加一个新Animal的来更改它。如果您被允许将 aList<Dog>用作 a ,List<Animal>那么您最终可能会得到一个也包含 a 的列表Cat

如果List<>不能突变(如在 Scala 中),那么您可以将 AList<Dog>视为List<Animal>. 例如,C# 使用协变和逆变泛型类型参数使这种行为成为可能。

这是更一般的Liskov 替换原则的一个实例。

突变导致您出现问题的事实发生在其他地方。考虑类型SquareRectangle

Square一个Rectangle吗?当然——从数学的角度来看。

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

您甚至可以添加基于这些属性计算其areaor的方法。perimeter

然后,您可以定义一个Square子类Rectangle并生成两者getWidthgetHeight返回相同值的类。

但是当您开始通过setWidthor允许突变时会发生什么setHeight

现在,Square不再是Rectangle. 改变其中一个属性将不得不默默地改变另一个以保持不变性,并且将违反 Liskov 的替换原则。改变 a 的宽度Square会产生意想不到的副作用。为了保持正方形,您还必须更改高度,但您只要求更改宽度!

你不能使用你的Square,只要你可以使用Rectangle. 因此,在存在突变的情况下aSquare不是 a Rectangle

您可以创建一个Rectangle知道如何克隆具有新宽度或新高度的矩形的新方法,然后您Square可以Rectangle在克隆过程中安全地转移到 a ,但现在您不再改变原始值。

同样,当它的界面允许您向列表中添加新项目时, aList<Dog>不能是 a 。List<Animal>

于 2010-03-03T01:03:26.183 回答
1

这是因为泛型类型是不变的。

于 2010-02-27T10:10:25.070 回答
0

英文答案:

如果'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!!

为了具有子类型关系,必须满足“可转换性”/“可替代性”标准。

  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. 非法泛型替换(类型参数转换) - 后代中不支持的操作:

    // 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 编译器会检查这个:-/。你基本上是在告诉编译器“相信我,我知道我在做什么,这是类型安全的”。

  • 不变的位置

    • 可变字段的类型(内部输入和输出) - 可以被所有祖先和子类型类读写 - 读取是协变的,写入是逆变的;结果是不变的
    • (如果类型参数同时用于协变和逆变位置,则这会导致不变性)
于 2013-07-24T06:55:33.293 回答
-1

通过继承,您实际上是在为多个类创建通用类型。这里有一个常见的动物类型。您通过创建一个 Animal 类型的数组并保留相似类型的值(继承类型 dog、cat 等)来使用它。

例如:

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

…………

知道了?

于 2010-02-27T09:33:43.687 回答