来自Joshua Bloch的Effective Java,
协变只是意味着如果X是Y的子类型,那么X []也将是Y []的子类型。数组是协变的因为字符串是Object So的子类型
String[] is subtype of Object[]
不变性仅仅意味着X不是Y的子类型,
List<X> will not be subType of List<Y>.
我的问题是为什么决定在Java中使数组协变?还有其他SO帖子,例如Why are Arrays invariant, but Lists covariant?,但它们似乎专注于Scala,我无法关注。
答案 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}}的子类型,在Javaobject[]
中是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数组类型之间的子类型
答案 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参考。