关于Java:任何简单的方法来解释为什么我不能做List animals = new ArrayList ()

Any simple way to explain why I cannot do List<Animal> animals = new ArrayList<Dog>()?

本问题已经有最佳答案,请猛点这里访问。

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


想象你创建了一个狗的列表。然后您将此声明为list并将其交给同事。他,并非无理,相信他可以放一只猫进去。

然后他把它还给你,你现在有了一张狗的清单,其中有一只猫。随之而来的是混乱。

需要注意的是,由于列表的可变性,存在这种限制。在scala中(例如),您可以声明狗的列表是动物的列表。这是因为scala列表(默认情况下)是不可变的,所以在狗列表中添加一只猫可以为您提供一个新的动物列表。


我能给出的最好的外行回答是:因为在设计泛型时,他们不想重复对Java的数组类型系统做出的不安全的决定。

这在数组中是可能的:

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

由于数组的类型系统在Java中工作,所以这个代码编译得很好。它将在运行时提升一个ArrayStoreException

决定不允许非专利药品出现这种不安全行为。

其他地方也可以看到: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>


假设你能做到。有人把一个List交给他,合理地期望他能做的事情之一就是在上面加上一个Giraffe。当有人试图在animals中添加Giraffe时,会发生什么?运行时错误?这似乎违背了编译时输入的目的。


如果你不能改变列表,那么你的推理就完全正确了。不幸的是,List<>被强行操纵。这意味着您可以通过添加新的Animal来更改List。如果允许你使用List作为List,你可以得到一个包含Cat的列表。

如果List<>不能突变(如scala),那么你可以把List当作List来处理。例如,C通过协变和逆变的泛型类型参数使这种行为成为可能。

这是更一般的李斯科夫代换原理的一个例子。

事实上,突变导致你在这里的问题发生在其他地方。考虑类型SquareRectangle

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

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

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

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

但是当你开始允许通过setWidthsetHeight突变时会发生什么?

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

你不能在任何时候使用你的Square,只要你可以使用Rectangle。因此,在存在突变的情况下,一个Square不是一个Rectangle

你可以在Rectangle上做一个新的方法,知道如何用新的宽度或高度克隆矩形,然后你的Square可以在克隆过程中安全地转移到Rectangle上,但现在你不再改变原来的值了。

同样,当List的接口允许您向列表中添加新项目时,它不能是List


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

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);
}

其中TDog的实现。

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());
    }
}

在这个Container接口的上下文中,您所要求的是非常合理的:

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不自动执行这个操作呢?考虑一下这对Comparator意味着什么。

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如何比较两个猫?

那么,ContainerComparator有什么区别呢?容器产生T类型的值,而Comparator使用这些值。这些对应于类型参数的协变和反变用法。

有时在两个位置都使用类型参数,使得接口不变。

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默认为不变性。在变量、字段、参数或方法返回的类型上,必须明确选择与? extends X? super X的适当差异。

这是一个真正的难题——每当有人使用通用类型时,他们必须做出这个决定!当然,ContainerComparator的作者应该能够一次性地声明这一点。

这称为"声明站点差异",在scala中可用。

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

注意如果你有

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

那么,如果你能做到的话

1
List<Animal> animals = dogs;

这并不能使dogs变成List。动物的数据结构仍然是一个ArrayList,所以如果你试图将一个Elephant插入animals,你实际上是在将它插入一个ArrayList中,这是不起作用的(大象显然太大了;-)。


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


英语回答:

如果"ListList的话,前者必须支持(继承)后者的所有操作。添加cat可以对后者执行,但不能对前者执行。所以"是"关系失败了。

编程答案:

类型安全

一个保守的语言默认设计选项,用于阻止此损坏:

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的同一类型或子类型时,G可以代替G。相反,当u是t的同一类型或超类型时,G可以代替G。这是两种情况下的安全位置:

    • 协变位置:

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

      在这些情况下,允许具有如下冗余的类型参数的可替换性是安全的:

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

      通配符加上"extends"提供使用站点指定的协方差。

    • 控制变量位置:

      • 方法参数类型(输入到泛型类型)-子类型必须具有相同/更大的适应能力,以便在传递祖先的参数时不会中断
      • 类型参数上限(内部类型实例化)-子类型必须具有相同的/更大的适应能力,以便在祖先设置变量值时不会中断。

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

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

      通配符加上"super"给出了使用站点指定的反差。

    使用这两个习语需要开发人员额外的努力和注意,以获得"可替换性能力"。Java需要手动开发人员努力确保类型参数在共变/逆变位中真正使用(因此类型安全)。我不知道为什么-例如scala编译器检查这个:-/。你基本上是在告诉编译器"相信我,我知道我在做什么,这是类型安全的"。

    • 不变位置

      • 可变字段的类型(内部输入和输出)-可以由所有祖先和子类读写-读是协变的,写是逆变的;结果是不变的
      • (同样,如果类型参数同时用于协变和逆变位置,则会导致不变性)

    通过继承,实际上您正在为几个类创建公共类型。这里有一种常见的动物类型。您可以通过在动物类型中创建一个数组并保留类似类型的值(继承类型:狗、猫等)来使用它。

    如:

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

    ……

    知道了?