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

时间:2013-09-06 21:16:49

标签: java arrays generics language-design covariance

来自Joshua Bloch的Effective Java,

  1. 阵列在两个重要方面与通用类型不同。第一个数组是协变的。泛型是不变的。
  2. 协变只是意味着如果X是Y的子类型,那么X []也将是Y []的子类型。数组是协变的因为字符串是Object So的子类型

    String[] is subtype of Object[]

    不变性仅仅意味着X不是Y的子类型,

     List<X> will not be subType of List<Y>.
    
  3. 我的问题是为什么决定在Java中使数组协变?还有其他SO帖子,例如Why are Arrays invariant, but Lists covariant?,但它们似乎专注于Scala,我无法关注。

9 个答案:

答案 0 :(得分:140)

通过wikipedia

  

早期版本的Java和C#不包含泛型(a.k.a. parametric polymorphism)。

     

在这样的设置中,使数组不变可以排除有用的多态程序。   例如,考虑编写一个函数来混淆数组,或者使用元素上的Object.equals方法测试两个数组是否相等的函数。实现不依赖于存储在数组中的元素的确切类型,因此应该可以编写适用于所有类型数组的单个函数。很容易实现类型

的功能
boolean equalArrays (Object[] a1, Object[] a2);
void shuffleArray(Object[] a);
     

但是,如果将数组类型视为不变量,则只能在类型为Object[]的数组上调用这些函数。例如,人们无法改变一系列字符串。

     

因此,Java和C#都会协同处理数组类型。例如,在C#中string[]是[{1}}的子类型,在Java object[]中是String[]的子类型。

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

当引入仿制药时,由于this answer by Jon Skeet中指出的原因,它们故意不会变得协变:

  

不,Object[]不是List<Dog>。考虑一下你可以用List<Animal>做什么 - 你可以添加任何动物......包括一只猫。现在,你可以逻辑地将一只猫添加到一窝幼犬吗?绝对不是。

List<Animal>
     

突然间你有一只非常迷茫的猫。

维基百科文章中描述的使数组协变的最初动机并不适用于泛型,因为wildcards使得协方差(和逆变)的表达成为可能,例如:

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

答案 1 :(得分:27)

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

例如:

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

答案 2 :(得分:20)

可能是this帮助: -

泛型不协变

Java语言中的数组是协变的 - 这意味着如果Integer扩展Number(它确实如此),那么不仅Integer也是Number,而Integer []也是Number[],您可以自由传递或指定要求Integer[]的{​​{1}}。 (更正式地说,如果Number是Integer的超类型,那么Number[]Number[]的超类型。)您可能认为泛型类型也是如此 - Integer[]是超级类型List<Number>,您可以传递List<Integer>,其中List<Integer>是预期的。不幸的是,它没有那种方式。

事实证明它有一个很好的理由它不会那样工作:它会破坏应该提供的类型安全通用。想象一下,您可以将List<Number>分配给List<Integer>。 然后,以下代码将允许您将不是Integer的内容放入List<Number>

List<Integer>

因为ln是List<Integer> li = new ArrayList<Integer>(); List<Number> ln = li; // illegal ln.add(new Float(3.1415)); ,所以添加Float似乎是完全合法的。但是如果ln与List<Number>别名,那么它将破坏li定义中隐含的类型安全承诺 - 它是一个整数列表,这就是泛型类型不能协变的原因。

答案 3 :(得分:3)

阵列是协变的,至少有两个原因:

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

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

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

答案 4 :(得分:3)

参数类型的一个重要特征是能够编写多态算法,即无论其参数值如何,都在数据结构上运行的算法,例如Arrays.sort()

使用泛型,这是通过通配符类型完成的:

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

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

void sort(Comparable[]);

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

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

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

简而言之,泛型所体现的更新方法使得类型系统更复杂,但也更加静态类型安全,而旧方法更简单,并且静态类型更安全。该语言的设计者选择了更简单的方法,比关闭很少引起问题的类型系统中的小漏洞有更重要的事情要做。后来,当Java建立起来,并且需要处理紧迫的需求时,他们有资源为泛型做正确的事情(但是为数组更改它会破坏现有的Java程序)。

答案 5 :(得分:2)

泛型不变:来自JSL 4.10

  

...子类型不会扩展到泛型类型:T&lt;:U不会   意味着C<T>&lt ;: C<U> ...

还有几行,JLS还解释了阵列是协变的(第一个子弹):

4.10.3数组类型之间的子类型

enter image description here

答案 6 :(得分:1)

我的观点:当代码期望数组A []并且你给它B [],其中B是A的子类时,只需要担心两件事:当你读取数组元素时会发生什么,以及会发生什么如果你写它。因此,编写语言规则并不难以确保在所有情况下都保持类型安全(主要规则是如果您尝试将A粘贴到B []中,则可能抛出ArrayStoreException。但是,对于泛型,当你声明一个类SomeClass<T>时,可以在类的主体中使用T的任意数量的方式,我猜它太复杂了,无法工作排除所有可能的组合,以编写关于什么时候允许以及什么时候不允许的规则。

答案 7 :(得分:1)

我认为他们在第一个做出错误决定的时候做出了一个错误的决定。它打破了here描述的类型安全性,他们因为向后兼容而陷入困境,之后他们试图不为通用做出同样的错误。 这就是Joshua Bloch在“有效Java(第二版)”一书的第25项中更喜欢列表的原因之一

答案 8 :(得分:0)

我们无法编写List<Object> l = new ArrayList<String>();,因为Java试图 保护我们免受运行时异常的影响。您可能会认为这意味着我们无法写 Object[] o = new String[0];。事实并非如此。这段代码会编译:

Integer[] numbers = { new Integer(42)};
Object[] objects = numbers;
objects[0] = "forty two"; // throws ArrayStoreException

尽管代码可以编译,但在运行时会引发异常。带有数组,Java 知道数组中允许的类型。只是因为我们已将Integer[]分配给 Object[]不会改变Java知道它确实是Integer[]的事实。

由于类型擦除,我们对ArrayList没有这种保护。在运行时, ArrayList不知道允许的内容。因此,Java使用编译器来 首先避免出现这种情况。好,那为什么不加Java 这个知识要ArrayList吗?原因是向后兼容。也就是说,Java是 坚持不破坏现有代码。

OCP参考。