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

eag*_*arn 157 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>.
    
    Run Code Online (Sandbox Code Playgroud)

我的问题是为什么决定在Java中使数组协变?还有其他SO帖子,例如为什么阵列不变,但列出协变?,但他们似乎专注于Scala,我无法遵循.

Pau*_*ora 147

通过维基百科:

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

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

boolean equalArrays (Object[] a1, Object[] a2);
void shuffleArray(Object[] a);
Run Code Online (Sandbox Code Playgroud)

但是,如果将数组类型视为不变量,则只能在完全类型的数组上调用这些函数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?
Run Code Online (Sandbox Code Playgroud)

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

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

boolean equalLists(List<?> l1, List<?> l2);
void shuffleList(List<?> l);
Run Code Online (Sandbox Code Playgroud)

  • 那是对的.数组具有可重新类型,并根据需要抛出`ArrayStoreException.显然,这被认为是当时值得妥协的.与今天相比:回想起来,许多人认为阵列协方差是一个错误. (20认同)
  • 为什么“许多”认为这是一个错误?它比没有数组协方差有用得多。您多久看到一次 ArrayStoreException?它们非常罕见。具有讽刺意味的是,imo 是不可原谅的……在 Java 中犯过的最严重的错误之一是使用站点差异,即通配符。 (4认同)
  • @ScottMcKinney:"为什么"很多人"认为这是一个错误?" AIUI,这是因为数组协方差需要对所有数组赋值操作进行动态类型测试(虽然编译器优化可能有帮助吗?),这可能会导致显着的运行时开销. (3认同)
  • 是的,Arrays允许多态行为,但是,它确实引入了运行时激活(与泛型的编译时异常不同).例如:`Object [] num = new Number [4]; num [1] = 5; num [2] = 5.0f; NUM [3] = 43.4; 的System.out.println(Arrays.toString(NUM)); NUM [0] = "你好";` (2认同)
  • 谢谢,多米尼克,但从我的观察看来,“许多人”认为这是一个错误的原因更像是模仿其他一些人所说的话。同样,重新审视数组协方差,它比破坏性更有用。同样,Java 所犯的实际大错误是通过通配符使用站点通用差异。这造成的问题比我认为“许多人”愿意承认的要多。 (2认同)

Kat*_*ona 28

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

例如:

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
Run Code Online (Sandbox Code Playgroud)

如果通用集合允许这样做:

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
Run Code Online (Sandbox Code Playgroud)

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

String first = strings.get(0); // would cause ClassCastException, trying to assign 12 to String
Run Code Online (Sandbox Code Playgroud)

  • 我个人认为这个答案提供了正确的解释,说明为什么集合不能协变时数组是协变的。谢谢! (2认同)

Rah*_*thi 21

可能是这个帮助: -

泛型不是协变的

Java语言中的数组是协变的 - 这意味着如果Integer扩展Number(它确实如此),那么不仅Integer也是Number,而且Integer []也是a Number[],你可以自由地传递或分配Integer[]需要a的地方Number[].(更正式地说,如果Number是Integer的超类型,则Number[]是超类型Integer[].)您可能认为泛型类型也是如此 - 这List<Number>是一种超类型List<Integer>,并且您可以传递a List<Integer>a a List<Number>expected.不幸的是,它没有那种方式.

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

List<Integer> li = new ArrayList<Integer>();
List<Number> ln = li; // illegal
ln.add(new Float(3.1415));
Run Code Online (Sandbox Code Playgroud)

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

  • 我的问题是`为什么'是阵列变得协变.正如Sotirios所提到的,使用Arrays会在运行时获得ArrayStoreException,如果Arrays不变,那么我们可以在编译时检测到这个错误本身是正确的吗? (4认同)
  • 对于Arrays,在运行时会得到一个`ArrayStoreException`. (3认同)