任何简单的方法来解释为什么我不能做List <animal> animals = new ArrayList <dog>()?</dog> </animal>

时间:2010-02-27 09:04:11

标签: java generics oop covariance

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

13 个答案:

答案 0 :(得分:48)

想象一下,您创建了一个 Dogs 列表。然后,您将其声明为 List&lt; Animal&gt; ,并将其交给同事。他,并非无理,相信他可以在其中加入 Cat

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

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

答案 1 :(得分: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会从动物派生出来,所以编译时检查会建议将它添加到List中。但是,在运行时,您无法将Cat添加到Dogs列表中!

所以,虽然它看似直观简单,但实际上这些问题实际上非常复杂。

这里有一个关于.NET 4协同/反演的MSDN概述:http://msdn.microsoft.com/en-us/library/dd799517(VS.100).aspx - 它也适用于java,虽然我不知道Java的支持是什么。

答案 2 :(得分:8)

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

这可以通过数组实现:

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

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

决定不允许对仿制药进行这种不安全的行为。

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

答案 3 :(得分:5)

列表&lt; Animal&gt; 是一个可以插入任何动物的对象,例如猫或章鱼。 ArrayList&lt; Dog&gt; 不是。

答案 4 :(得分:5)

您要做的是以下内容:

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

这应该有效。

答案 5 :(得分:4)

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

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>

答案 6 :(得分:3)

假设你可以这样做。有人递给List<Animal>合理期望能够做的事情之一是向其添加Giraffe。当有人试图将Giraffe添加到animals时会发生什么?运行时错误?这似乎打败了编译时输入的目的。

答案 7 :(得分:2)

请注意,如果你有

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

然后,如果你能做到

List<Animal> animals = dogs;

dogs变成List<Animal>。动物的数据结构仍为ArrayList<Dog>,因此,如果您尝试将Elephant插入animals,则实际上是将其插入到ArrayList<Dog>中,而不是{{1}}工作(大象显然太大了; - )。

答案 8 :(得分: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);
}

其中TDog的实现。

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>怎么能比较两只猫?

那么ContainerComparator之间有什么区别?容器生成类型T的值,而Comparator 消费。这些对应于类型参数的协变反变量用法。

有时在两个位置使用type参数,使界面不变

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明确选择适当的方差。

这是一个真正的麻烦 - 每当有人使用通用类型时,他们必须做出这个决定!当然ContainerComparator的作者应该能够一劳永逸地宣布这一点。

这称为“声明站点差异”,可在Scala中找到。

trait Container[+T] { ... }
trait Comparator[-T] { ... }

答案 9 :(得分:2)

如果您无法改变列表,那么您的推理将完全合理。不幸的是,List<>被强制操纵。这意味着您可以通过向其添加新List<Animal>来更改Animal。如果您被允许使用List<Dog>作为List<Animal>,则可以使用包含Cat的列表。

如果List<>无法发生变异(例如Scala中),那么您可以将A List<Dog>视为List<Animal>。例如,C#使用协变和逆变泛型类型参数使这种行为成为可能。

这是更通用的Liskov substitution principal的实例。

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

SquareRectangle吗?当然 - 从数学的角度来看。

您可以定义Rectangle类,该类提供可读的getWidthgetHeight属性。

您甚至可以根据这些属性添加计算其areaperimeter的方法。

然后,您可以定义一个Square类,其子类Rectangle并使getWidthgetHeight返回相同的值。

但是当您开始通过setWidthsetHeight进行变异时会发生什么?

现在,Square不再是Rectangle的合理子类。改变其中一个属性必须默默地改变另一个以保持不变量,并且Liskov的替换委托人将被违反。更改Square的宽度会产生意外的副作用。为了保持正方形,你也必须改变高度,但你只要求改变宽度!

无论何时使用Square,您都无法使用Rectangle。所以,存在突变 Square不是Rectangle

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

同样地,当List<Dog>的界面授权您将新项目添加到列表中时,List<Animal>不能是{{1}}。

答案 10 :(得分:1)

这是因为泛型类型是invariant

答案 11 :(得分: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. 非法的通用替换(类型参数的强制转换) - 在decendant中不支持的操作:

    // 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
    

  4. 然而

    根据通用类的设计,类型参数可用于“安全位置”,这意味着有时可以成功进行转换/替换而不会破坏类型安全性。协方差意味着,如果U是T的相同类型或子类型,则通用instatition G<U>可以替换G<T>。反对方法意味着如果U是相同类型,则通用实例G<U>可以替代G<T>或T的超类型。这些是2例的安全位置:

    • 协变职位:

      • 方法返回类型(泛型类型的输出) - 子类型必须具有相同/更严格的限制,因此它们的返回类型符合祖先
      • 不可变字段的类型(由所有者类设置,然后'内部仅输出') - 子类型必须更具限制性,因此当它们设置不可变字段时,它们符合祖先

      在这些情况下,允许使用类似于上风的类型参数的可替换性是安全的:

      SomeCovariantType<Dog> decendant = new SomeCovariantType<>;
      SomeCovariantType<? extends Animal> ancestor = decendant;
      

      通配符加'extends'为使用地点指定了协方差。

    • 控制性职位:

      • 方法参数类型(输入到泛型类型) - 子类型必须同等/更适应,以便在传递祖先的参数时不会中断
      • 上层类型参数边界(内部类型实例化) - 子类型必须同等/更适应,因此当祖先设置变量值时它们不会中断

      在这些情况下,允许类型参数的可替换性与这样的祖先是安全的:

      SomeContravariantType<Animal> decendant = new SomeContravariantType<>;
      SomeContravariantType<? super Dog> ancestor = decendant;
      

      通配符加上'super'给出了使用地点指定的逆变。

    使用这两个成语需要开发人员额外的努力和关心以获得“替代能力”。 Java需要手动开发人员努力以确保类型参数分别真正用于协变/逆变位置(因此类型安全)。我不知道为什么 - 例如scala编译器检查这个: - /。你基本上是在告诉编译器“相信我,我知道我在做什么,这是类型安全的”。

    • 不变的职位

      • 可变字段的类型(内部输入和输出) - 可以由所有祖先和子类型读取和写入 - 读取是协变的,写入是逆变的;结果是不变的
      • (如果在协变和逆变位置都使用了类型参数,那么这会导致不变性)

答案 12 :(得分:-1)

通过继承,您实际上正在为多个类创建公共类型。在这里你有一个共同的动物类型。你是通过在Animal类型中创建一个数组并保持相似类型的值(继承类型dog,cat等)来使用它。

例如:

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

.......

知道了吗?