为什么数组是协变的,但泛型是不变的?
来自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帖子,比如为什么数组是不变的,但是列表是协变的? ,但他们似乎集中在斯卡拉,我不能遵循。
通过维基百科 :
Java和C#的早期版本不包含泛型(aka参数多态)。
在这样的设置下,使数组不变规则排除有用的多态程序。 例如,考虑编写一个函数来对数组进行混洗,或者使用元素上的
Object.equals
方法来测试两个数组是否相等。 实现不依赖于数组中存储的元素的确切类型,所以应该可以编写一个适用于所有类型数组的函数。 很容易实现类型的功能boolean equalArrays (Object[] a1, Object[] a2); void shuffleArray(Object[] a);
但是,如果将数组类型视为不变量,则只能将这些函数调用到类型为
Object[]
的数组上。 例如,人们不能洗牌一串字符串。因此,Java和C#共同处理数组类型。 例如,在C#中,
string[]
是object[]
的子类型,而在Java中,String[]
是Object[]
的子类型。
这回答了“为什么是数组协变?”的问题,或者更准确地说,“为什么数组在当时是协变的 ?
当引入泛型时,由于Skeet在这个答案中指出的原因,他们故意没有变得协变:
不,
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?
突然间,你有一只很困惑的猫。
在维基百科文章中描述的使数组协变的原始动机不适用于泛型,因为通配符使协变(和变换)的表达成为可能,例如:
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
但是,如果有人尝试访问列表,则会导致问题。
String first = strings.get(0); // would cause ClassCastException, trying to assign 12 to String
可能是这个帮助:
泛型不是协变的
Java语言中的数组是协变的 – 这意味着如果Integer扩展了Number(它的作用),那么不仅Integer也是一个Number,而Integer []也是一个Number[]
,你可以自由地通过或者在一个Number[]
被调用的地方分配一个Integer[]
。 (更正式地说,如果Number是Integer的超类型,那么Number[]
是Integer[]
的超类型Integer[]
。您也许会认为泛型类型也是如此 – List<Number>
是List<Integer>
的超类型List<Integer>
,并且您可以传递List<Integer>
,其中List<Number>
是预期的。 不幸的是,这并不是那种方式。
事实证明,有一个很好的理由,它不会这样工作:它会打破类型安全仿制药应该提供的。 想象一下,您可以将List<Integer>
分配给List<Number>
。 然后下面的代码将允许你把一个不是整数的东西放到一个List<Integer>
:
List<Integer> li = new ArrayList<Integer>(); List<Number> ln = li; // illegal ln.add(new Float(3.1415));
因为ln是一个List<Number>
,所以添加一个Float似乎是完全合法的。 但是如果ln与li
混淆在一起,那么它将打破li定义中隐含的类型安全承诺 – 它是一个整数列表,这就是为什么泛型不能协变的原因。
阵列是协变的,至少有两个原因:
-
对于保存永远不会变化的信息的收集是有益的。 对于一个T是协变的集合,它的支持存储也必须是协变的。 虽然可以设计一个不使用
T[]
作为其后备存储的不可变T
集合(例如,使用树或链表),但这样的集合不太可能像数组所支持的那样。 有人可能会争辩说,提供协变不可变集合的更好的方法是定义一个“协变不变的数组”类型,他们可以使用后备存储,但只是允许数组协变可能更容易。 -
数组经常会被代码所突变,这些代码不知道将要在其中的东西是什么类型的东西,但是不会把没有从同一个数组中读出的东西放进数组中。 一个很好的例子就是排序代码。 从概念上讲,数组类型可能包含交换或置换元素的方法(这种方法可能同样适用于任何数组类型),或者定义一个“数组操作符”对象,该对象持有对数组和一个或多个事物的引用已经从它读取,并可能包括方法来存储以前读取的项目,他们来了阵列。 如果数组不是协变的,用户代码将无法定义这样的类型,但运行时可能包含一些专门的方法。
数组是协变的事实可能被看作是一个丑陋的黑客攻击,但是在大多数情况下,它有助于创建工作代码。
参数类型的一个重要特征是能够编写多态算法,即对数据结构进行操作的算法,而不管其参数值如何,如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程序)。
我的意思是:当代码需要一个数组A [],并且给它B [],其中B是A的一个子类时,只有两件事值得担心:读数组元素时会发生什么,如果写它。 因此,编写语言规则并不难,以确保在所有情况下保持类型安全(主要规则是如果您试图将A粘贴到B []中,则可能抛出ArrayStoreException
)。 对于泛型而言,当你声明一个类SomeClass<T>
,在类的主体中可以使用任意数量的方法T
,而且我猜这只是太复杂而无法解决所有可能的组合编写关于什么时候什么时候被允许和什么时候不允许的规则。
泛型是不变的 :从JSL 4.10 :
…子类型不扩展到泛型:T <:U并不意味着
C<T>
<:C<U>
…
还有几行,JLS还解释说
阵列是协变的 (第一颗子弹):
4.10.3数组类型之间的子类型化
我认为他们首先做出了一个错误的决定,使得协变。 它打破了这里所描述的类型安全性,因为后向兼容性而陷入了困境,之后他们试图不为泛型做出同样的错误。 这就是Joshua Bloch喜欢列入“有效的Java(第二版)”第25条中列举的原因之一