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

Java 为什么数组是协变的,而泛型是不变的?,java,arrays,generics,language-design,covariance,Java,Arrays,Generics,Language Design,Covariance,摘自Joshua Bloch的《有效Java》 数组在两个重要方面不同于泛型类型。第一个数组是协变的。泛型是不变的 协变仅仅意味着如果X是Y的子类型,那么X[]也将是Y[]的子类型。数组是协变的,因为字符串是对象的子类型,所以 String[]是对象[]的子类型 不变量只是指无论X是否为Y的子类型 List<X> will not be subType of List<Y>. 列表将不是列表的子类型。 我的问题是为什么在Java中决定让数组协变?还有其他一些SO帖子

摘自Joshua Bloch的《有效Java》

  • 数组在两个重要方面不同于泛型类型。第一个数组是协变的。泛型是不变的
  • 协变仅仅意味着如果X是Y的子类型,那么X[]也将是Y[]的子类型。数组是协变的,因为字符串是对象的子类型,所以

    String[]是对象[]的子类型

    不变量只是指无论X是否为Y的子类型

     List<X> will not be subType of List<Y>.
    
    列表将不是列表的子类型。
    
  • 我的问题是为什么在Java中决定让数组协变?还有其他一些SO帖子,如,但它们似乎集中在Scala上,我无法跟上。

    可能会有所帮助:-

    泛型不是协变的

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

    事实证明,有一个很好的理由表明它不能以这种方式工作:它会破坏泛型应该提供的类型安全性。想象一下,您可以将
    列表
    分配给
    列表
    。 然后,以下代码将允许您将非整数的内容放入
    列表中

    List li=new ArrayList();
    列表ln=li;//非法的
    增加(新浮动(3.1415));
    
    因为ln是一个
    列表
    ,所以在其中添加浮点数似乎是完全合法的。但是,如果ln的别名为
    li
    ,那么它将打破li定义中隐含的类型安全承诺——它是一个整数列表,这就是泛型类型不能协变的原因。

    通过:

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

    在这样的设置中,使数组不变排除了有用的多态程序。 例如,考虑编写一个函数来拖曳一个数组,或者使用一个函数来测试两个数组,以便使用<代码>对象。实现不依赖于存储在数组中的元素的确切类型,因此应该可以编写一个适用于所有类型数组的函数。类型的函数很容易实现

    boolean equalArrays (Object[] a1, Object[] a2);
    void shuffleArray(Object[] a);
    
    但是,如果数组类型被视为不变量,则只能在类型正好为
    Object[]
    的数组上调用这些函数。例如,不能洗牌字符串数组

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

    这就回答了“为什么阵列是协变的?”,或者更准确地说,“为什么阵列在当时是协变的?”

    当引入泛型时,出于下列原因,它们故意不协变:

    否,
    列表
    不是使协方差(和反方差)表达式成为可能的
    列表,例如:

    boolean equalLists(List<?> l1, List<?> l2);
    void shuffleList(List<?> l);
    
    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
    
    布尔等式列表(列表l1、列表l2);
    无效随机列表(列表l);
    
    原因是每个数组在运行时都知道其元素类型,而泛型集合则不知道,因为类型擦除

    例如:

    boolean equalLists(List<?> l1, List<?> l2);
    void shuffleList(List<?> l);
    
    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
    

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

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

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


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

    我的观点:当代码需要一个数组A[]而你给它B[],其中B是A的子类时,只需要担心两件事:读取数组元素时会发生什么,如果你写下来会发生什么。因此,编写语言规则以确保在所有情况下都能保持类型安全并不困难(主要规则是,如果您试图将A插入B[],则可能会抛出
    ArrayStoreException
    )。但是,对于泛型,当您声明一个类
    SomeClass
    String first = strings.get(0); // would cause ClassCastException, trying to assign 12 to String
    
    <E extends Comparable<E>> void sort(E[]);
    
    void sort(Comparable[]);
    
    String[] strings = {"hello"};
    Object[] objects = strings;
    objects[0] = 1; // throws ArrayStoreException
    
    Integer[] numbers = { new Integer(42)};
    Object[] objects = numbers;
    objects[0] = "forty two"; // throws ArrayStoreException