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


160

摘自Joshua Bloch的Effective Java,

  1. 数组在两个重要方面不同于通用类型。第一数组是协变的。泛型是不变的。
  2. 协变量仅表示如果X是Y的子类型,则X []也将是Y []的子类型。数组是协变的,因为字符串是Object的子类型,所以

    String[] is subtype of Object[]

    不变式仅表示X是否为Y的子类型,

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

我的问题是为什么要在Java中使数组协变的决定?还有其他SO帖子,例如“ 为什么数组不变”,但“列表协变”?,但是它们似乎专注于Scala,我无法跟踪。


1
这不是因为后来添加了泛型吗?
Sotirios Delimanolis 2013年

1
我认为比较数组和集合是不公平的,集合在后台使用数组!
艾哈迈德·阿德尔·伊斯梅尔

4
@ EL-conteDe-monteTereBentikh例如,并非所有集合LinkedList
保罗·贝洛拉

@PaulBellora我知道Maps与Collection的实现者不同,但是我在SCPJ6中读到Collections通常依赖于数组!
艾哈迈德·阿德尔·伊斯梅尔

因为没有ArrayStoreException; 当在Collection中插入错误的元素时(作为数组)。因此,Collection只能在检索时找到它,而且由于转换也可以找到它。因此,泛型将确保解决此问题。
Kanagavelu Sugumar '16

Answers:


150

通过维基百科

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

在这种情况下,使数组不变会排除有用的多态程序。例如,考虑编写一个对数组进行混洗的函数,或者使用Object.equals元素上的方法测试两个数组是否相等的函数。实现方式不依赖于存储在数组中的元素的确切类型,因此应该有可能编写一个可以在所有类型的数组上使用的函数。实现类型的功能很容易

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

但是,如果将数组类型视为不变的,则只能在类型完全相同的数组上调用这些函数Object[]。例如,无法将一组字符串混排。

因此,Java和C#都会协变地对待数组类型。例如,在C#中string[]是的子类型object[],在Java中String[]是的子类型Object[]

这回答了问题:“为什么是数组协变的?”,或者更准确的说,“为什么由协阵列的时候?”

当引入泛型时,出于Jon Skeet此答案中指出的原因,有意地使它们无协变:

不,a 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);

3
是的,数组允许多态行为,但是,它确实引入了运行时特权(与泛型的编译时异常不同)。例如:Object[] num = new Number[4]; num[1]= 5; num[2] = 5.0f; num[3]=43.4; System.out.println(Arrays.toString(num)); num[0]="hello";
eagerto13年

21
没错 数组具有可更改的类型,并ArrayStoreException根据需要抛出。显然,这在当时被认为是值得的妥协。与今天相反:回想起来,许多人将数组协方差视为错误。
Paul Bellora 2013年

1
为什么“很多”认为这是一个错误?这比没有数组协方差要有用得多。您多久见一次ArrayStoreException; 他们是非常罕见的。具有讽刺意味的是,这是不可原谅的imo ...在Java中曾经犯过的最严重的错误是使用站点差异(也称为通配符)。
斯科特(Scott)

3
@ScottMcKinney:“为什么“很多”认为这是一个错误?” AIUI,这是因为数组协方差要求对所有数组赋值操作进行动态类型测试(尽管编译器优化可能会有所帮助?),这可能会导致大量的运行时开销。
Dominique Devriese 2015年

谢谢,多米尼克(Dominique),但根据我的观察,似乎“很多”认为这是一个错误的原因更像是模仿其他人所说的话。再次,重新审视数组协方差,它远比损坏有用。同样,Java犯下的实际BIG错误是通配符引起的使用现场通用方差。这引起了比我认为“许多”想要承认的问题更多的问题。
斯科特,

30

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

例如:

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

我认为Paul Bellora的答案更合适,因为他对WHY数组的评论是协变的。如果数组是不变的,那就好了。您将对其进行类型擦除。类型为Erasure属性的主要原因是向后兼容性正确吗?
eagertoLearn

@ user2708477,是的,由于向后兼容而引入了类型擦除。是的,我的答案试图回答标题中的问题,为什么泛型是不变的。
卡托纳2013年

数组知道其类型这一事实意味着,尽管协方差允许代码要求将某些内容存储到不适合存储的数组中,但这并不意味着将允许进行此类存储。因此,通过使数组为协变而引入的危险程度远小于不知道其类型的情况。
2013年

@supercat,正确的,我想指出的是,对于具有适当类型擦除的泛型,使用最小的运行时检查安全性就无法实现协方差
Katona

1
我个人认为此答案提供了正确的解释,即为什么不能使用Collections时为什么数组是协变的。谢谢!
asgs

22

可能是这样的帮助:-

泛型不是协变的

Java语言中的数组是协变的-这意味着,如果Integer扩展Number(它可以这样做),那么Integer不仅是Number,而且Integer []也是a Number[],您可以自由传递或分配一个Integer[]需要a的地方Number[]。(更正式地说,如果Number是Integer的超类型,则它Number[]是。的超类型Integer[]。)您可能会认为通用类型也是如此-这List<Number>是的超类型List<Integer>,并且您可以List<Integer>List<Number>期望的a 处传递a 。不幸的是,它不能那样工作。

事实证明,这样做有一个很好的理由:它会破坏安全性泛型应该提供的类型。假设您可以将分配List<Integer>List<Number>。然后,以下代码将允许您将非整数的内容放入List<Integer>

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

因为ln是a List<Number>,所以向其添加Float似乎是完全合法的。但是,如果ln别名为li,则它将破坏li定义中隐含的类型安全保证-它是一个整数列表,这就是为什么泛型不能协变的原因。


3
对于数组,您将ArrayStoreException在运行时获得一个。
Sotirios Delimanolis 2013年

4
我的问题是WHY数组使协变。正如Sotirios所提到的,使用Arrays可以在运行时获取ArrayStoreException,如果将Arrays设为不变,那么我们可以在编译时本身检测到该错误吗?
eagertoLearn 2013年

@eagertoLearn:Java的一个主要语义弱点是,它的类型系统中的任何内容都无法将“除了它的派生类之外什么都没有的数组Animal,它不必接受从其他地方接收到的任何项目”与“除了什么都不能包含Animal,并且必须愿意接受外部提供的对Animal。的引用,需要前者的代码应接受的数组Cat,但需要后者的代码则不应接受。如果编译器可以区分这两种类型,则可以提供编译时检查。不幸的是,唯一使他们与众不同的地方
超级猫

...是代码是否实际上试图在其中存储任何内容,并且直到运行时才知道。
超级猫

3

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

  • 这对于保存永远不变的信息的集合很有用。为了使T的集合是协变的,其后备存储也必须是协变的。尽管可以设计一个不可变的T集合,而不使用a T[]作为其后备存储(例如,使用树或链表),但这样的集合不太可能像由数组支持的那样表现良好。有人可能会说,提供协变不可变集合的一种更好的方法是定义一个可以使用后备存储的“协变不可变数组”类型,但是简单地允许数组协变可能会更容易。

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

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


1
The fact that arrays are covariant may be viewed as an ugly hack, but in most cases it facilitates the creation of working code.-好点
eagertoLearn 2013年

3

参数类型的重要特征是能够编写多态算法,即,无论数据结构的参数值如何(例如)对数据结构进行操作的算法Arrays.sort()

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

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

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

void sort(Comparable[]);

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

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

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

简而言之,泛型实现的新方法使类型系统更复杂,但也使静态类型安全性更高,而较旧的方法更简单,而静态类型安全性更差。语言的设计者选择了更简单的方法,比关闭类型系统中很少引起问题的小漏洞,还有更重要的事情要做。后来,当Java建立并满足了紧迫的需求时,他们就有资源将其正确地用于泛型(但是将其更改为数组会破坏现有的Java程序)。


2

泛型是不变的:从JSL 4.10开始

...子类型化不扩展到泛型类型:T <:U并不意味着C<T><:C<U>...

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

4.10.3数组类型之间的子类型化

在此处输入图片说明


2

我认为他们一开始就做出了错误的决定,导致数组协变。它破坏了这里描述的类型安全性,并且由于向后兼容而被卡住了,此后,他们试图避免对泛型犯同样的错误。这就是约书亚·布洛赫(Joshua Bloch)更喜欢列表而不是“有效Java(第二版)”第25条中的原因之一。


Josh Block是Java的collections框架(1.2)的作者,也是Java的泛型(1.5)的作者。因此,构建每个人都抱怨的仿制药的人也恰巧是写这本书的人,说他们是更好的方法吗?不足为奇!
cpurdy

1

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

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.