为什么数组是协变的,而泛型是不变的?

来自Joshua Bloch的Effective Java,

  1. 数组在两个重要方面不同于泛型类型。第一个数组是协变的。泛型是不变的。
  2. 协变简单地意味着如果X是Y的子类型,那么X[]也将是Y[]的子类型。数组是协变的,因为字符串是对象的子类型,所以

    String[] is subtype of Object[]

    不变量简单地意味着不考虑X是否是Y的子类型,

     List<X> will not be subType of List<Y>.
    

My question is why the decision to make arrays covariant in Java? There are other SO posts such as Why are Arrays invariant, but Lists covariant?, but they seem to be focussed on Scala and I am not able to follow.

43058 次浏览

可能帮助:-

泛型不是协变的。

Java语言中的数组是协变的,这意味着如果Integer扩展了Number(确实如此),那么不仅Integer是一个Number,而且Integer[]还是一个Number[],您可以自由地将Integer[]传递或赋值给调用Number[]的地方。(更正式地说,如果Number是Integer的超类型,则Number[]Integer[]的超类型。)您可能认为泛型类型也是如此--List<Number>List<Integer>的超类型,并且您可以在期望List<Number>的地方传递List<Integer>。不幸的是,它不是这样工作的。

事实证明,有一个很好的理由说明它不能以这种方式工作:它会破坏泛型应该提供的类型安全。假设您可以将List<Integer>分配给List<Number>。 则下面的代码将允许您将不是整数的内容放入List<Integer>中:

List<Integer> li = new ArrayList<Integer>();
List<Number> ln = li; // illegal
ln.add(new Float(3.1415));

因为ln是_0的_ABC,所以给它添加一个浮点数似乎是完全合法的。但是,如果ln被赋予li的别名,那么它将破坏隐含在Li的定义中的类型安全承诺--它是一个整数列表,这就是泛型类型不能协变的原因。

通过维基百科

Java和C#的早期版本不包括泛型(也称为参数多态性)。

在这种情况下,使数组保持不变就排除了有用的多态程序。 例如,考虑编写一个用于混排数组的函数,或者编写一个对元素使用Object.equals方法来测试两个数组是否相等的函数。该实现并不依赖于存储在数组中的元素的确切类型,因此应该可以编写一个适用于所有类型的数组的函数。类型

的函数很容易实现。
boolean equalArrays (Object[] a1, Object[] a2);
void shuffleArray(Object[] a);

但是,如果将数组类型视为不变类型,则只能对类型Object[]的数组调用这些函数。例如,不能对字符串数组进行混洗。

因此,Java和C#都以协变的方式处理数组类型。例如,在C#_中,ABC_0是object[]的子类型,而在Java_中,ABC_2是Object[]的子类型。

这就回答了“为什么数组是协变的?”这个问题,或者更准确地说,“为什么,数组变得协变当时?”

当引入泛型时,由于乔恩·斯基特的回答中指出的原因,故意不使它们协变:

否,List<Dog>不是List<Animal>。考虑一下你能用List<Animal>做什么——你可以把任何动物加进去……包括一只猫。现在,你能合乎逻辑地把一只猫加到一窝小狗上吗?绝对不行。

// Illegal code - because otherwise life would be Bad
List<Dog> dogs = new List<Dog>();
List<Animal> animals = dogs; // Awooga awooga
animals.add(new Cat());
Dog dog = dogs.get(0); // This should be safe, right?

突然你有一个非常困惑的猫。

Wikipedia文章中描述的使数组协变的最初动机并不适用于泛型,因为通配符使得协变(和逆变)的表达式成为可能,例如:

boolean equalLists(List<?> l1, List<?> l2);
void shuffleList(List<?> l);

原因是每个数组在运行时都知道它的元素类型,而泛型集合由于类型擦除而不知道。

例如:

String[] strings = new String[2];
Object[] objects = strings;  // valid, String[] is Object[]
objects[0] = 12; // error, would cause java.lang.ArrayStoreException: java.lang.Integer during runtime

如果泛型集合允许这样做:

List<String> strings = new ArrayList<String>();
List<Object> objects = strings;  // let's say it is valid
objects.add(12);  // invalid, Integer should not be put into List<String> but there is no information during runtime to catch this

但是,当有人试图访问该列表时,这会导致问题:

String first = strings.get(0); // would cause ClassCastException, trying to assign 12 to String

数组是协变的,至少有两个原因:

  • 它对于包含永远不会改变为协变的信息的集合非常有用。要使T的集合是协变的,其后备存储也必须是协变的。虽然可以设计不使用T[]作为其后备存储(例如,使用树或链表)的不可变T集合,但是这样的集合不太可能执行得像由数组支持的集合一样好。有人可能会争辩说,提供协变不变集合的更好方法是定义“协变不变数组”类型。他们可以使用后备存储,但简单地允许数组协变可能更容易。

  • 数组经常会被代码改变,这些代码不知道数组中会有什么类型的东西,但不会将任何不是从同一数组中读取的东西放入数组。这方面的一个主要例子是排序代码。从概念上讲,数组类型可以包含交换或置换元素的方法(这样的方法同样适用于任何数组类型),或者定义一个“数组操纵器”对象,该对象保存对数组的引用以及从数组中读取的一个或多个内容,并且可以包含将以前读取的项目存储到它们所来自的数组中的方法。如果数组不是协变的,用户代码将无法定义这样的类型,但运行时可以包含一些专门的方法。

数组是协变的这一事实可能会被视为一种丑陋的黑客行为,但在大多数情况下,它有助于创建工作代码。

我的看法:当代码需要一个数组a[],而你给它B[],其中B是a的子类时,只需要担心两件事:当你读取一个数组元素时会发生什么,以及如果你写它会发生什么。因此,不难编写语言规则来确保在所有情况下都保持类型安全(主要规则是,如果您试图将A插入B[],则可能会引发ArrayStoreException)。然而,对于泛型,当您声明ABC_1_类时,在类体中使用ABC_2_可以有多种方式,我猜这太复杂了,无法计算出所有可能的组合来编写关于何时允许和何时不允许的规则。

参数类型的一个重要特征是能够编写多态算法,即对数据结构进行操作而不考虑其参数值的算法,例如Arrays.sort()

对于泛型,这是通过通配符类型完成的:

<E extends Comparable<E>> void sort(E[]);

要真正有用,通配符类型需要通配符捕获,而这需要类型参数的概念。当数组被添加到Java中时,这些都是不可用的,并且引用类型协变的数组允许一种简单得多的方法来允许多态算法:

void sort(Comparable[]);

然而,这种简单性在静态类型系统中打开了一个漏洞:

String[] strings = {"hello"};
Object[] objects = strings;
objects[0] = 1; // throws ArrayStoreException

需要对引用类型数组的每次写访问进行运行时检查。

简而言之,泛型体现的新方法使类型系统更复杂,但也更静态类型安全,而旧方法更简单,静态类型安全性更低。该语言的设计者选择了更简单的方法,有更重要的事情要做,而不是在类型系统中填补一个很少引起问题的小漏洞。后来,当Java建立时,迫切的需求得到了满足,他们有足够的资源来正确地处理泛型(但将其更改为数组会破坏现有的Java程序)。

泛型是不变的:来自JSL 4.10

。.。子类型不通过泛型类型扩展:t<;:u不 简化C<T>C<U>..

此外,JLS还解释了
数组是协变的(第一个项目符号):

4.10.3数组类型中的子类型

enter image description here

我认为他们一开始做了一个错误的决定,使得数组协变。它破坏了类型安全,因为它描述了在这里,由于向后兼容性,他们陷入了困境,在那之后,他们试图不在泛型上犯同样的错误。 这就是约书亚·布洛赫在“ Effective Java(第二版)”

一书的第25条中更喜欢列表而不是Arra ys的原因之一。