188

来自 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 帖子,例如Why are Arrays invariant, but Lists covariant? ,但他们似乎专注于 Scala,我无法遵循。

4

9 回答 9

179

通过维基百科

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

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

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

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

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

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

当引入泛型时,由于 Jon Skeet在这个答案中指出的原因,故意不使它们成为协变的:

不, aList<Dog>不是 a List<Animal>。考虑一下你可以用 a 做什么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?

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

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

boolean equalLists(List<?> l1, List<?> l2);
void shuffleList(List<?> l);
于 2013-09-06T21:30:49.837 回答
34

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

例如:

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
于 2013-09-06T21:34:03.780 回答
24

可能是这样的帮助: -

泛型不是协变的

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

事实证明,它不能那样工作是有充分理由的:它会破坏泛型应该提供的类型安全。想象一下,您可以将 a 分配List<Integer>给 a List<Number>。然后下面的代码将允许您将不是 Integer 的内容放入 a 中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 定义中隐含的类型安全承诺——它是一个整数列表,这就是泛型类型不能协变的原因。

于 2013-09-06T21:21:59.970 回答
3

参数类型的一个重要特征是编写多态算法的能力,即对数据结构进行操作而不管其参数值如何的算法,例如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 程序)。

于 2013-09-13T17:48:27.400 回答
3

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

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

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

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

于 2013-09-06T21:36:01.690 回答
3

我认为他们首先做出了错误的决定,使数组协变。它破坏了这里描述的类型安全,由于向后兼容性,他们陷入了困境,之后他们试图不对泛型犯同样的错误。这也是Joshua Bloch在“Effective Java(第二版)”一书的第 25 项中更喜欢列表而不是数组的原因之一

于 2016-04-01T19:22:08.683 回答
2

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

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

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

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

在此处输入图像描述

于 2014-06-15T17:37:10.647 回答
1

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

于 2013-09-06T21:56:39.543 回答
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 知道数组中允许的类型。仅仅因为我们将 an 分配Integer[]给 anObject[]并不会改变 Java 知道它实际上是一个Integer[].

由于类型擦除,我们对 ArrayList 没有这种保护。在运行时,ArrayList 不知道其中允许什么。因此,Java 使用编译器从一开始就防止这种情况出现。好的,那么为什么 Java 不将这些知识添加到 ArrayList 中呢?原因是向后兼容;也就是说,Java 在不破坏现有代码方面很重要。

OCP 参考。

于 2020-08-30T16:56:16.953 回答