变性是OOP言语稳定的大坑,Java的数组协变就是个中的一口老坑。由于近来踩到了,便做一个纪录。趁便也提一下范型的变性。
诠释数组协变之前,先明白三个相干的观点,协变、稳定和逆变。
一、协变、稳定、逆变
假定,我为一家餐馆写了如许一段代码
class Soup<T> { public void add(T t) {} } class Vegetable { } class Carrot extends Vegetable { }
有一个范型类Soup<T>,示意用食材T做的汤,它的要领add(T t)示意向汤中增加食材T。类Vegetable示意蔬菜,类Carrot示意胡萝卜。固然,Carrot是Vegetable的子类。
那末题目来了,Soup<Vegetable>和Soup<Carrot>之间是什么关联呢?
第一回响反映,Soup<Carrot>应当是Soup<Vegetable>的子类,由于胡萝卜汤显然是一种蔬菜汤。假如真是如许,那就看看下面的代码。个中Tomato示意西红柿,是Vegetable的另一个子类
Soup<Vegetable> soup = new Soup<Carrot>(); soup.add(new Tomato());
第一句没题目,Soup<Carrot>是Soup<Vegetable>的子类,所以能够将Soup<Carrot>的实例赋给变量soup。第二句也没题目,由于soup声明为Soup<Vegetable>范例,它的add要领吸收一个Vegetable范例的参数,而Tomato是Vegetable,范例准确。
然则,两句放在一同却有了题目。soup的现实范例是Soup<Carrot>,而我们给它的add要领通报了一个Tomato的实例!换言之,我们在用西红柿做胡萝卜汤,肯定做不出来。所以,把Soup<Carrot>视为Soup<Vegetable>的子类在逻辑上虽然是通畅的,在运用过程中倒是有缺点的。
那末,Soup<Carrot>和Soup<Vegetable>终究应当是什么关联呢?差别的言语有差别的明白和完成。总结起来,有三种状况。
(1)假如Soup<Carrot>是Soup<Vegetable>的子类,则称泛型Soup<T>是协变的
(2)假如Soup<Carrot>和Soup<Vegetable>是无关的两个类,则称泛型Soup<T>是稳定的
(3)假如Soup<Carrot>是Soup<Vegetable>的父类,则称泛型Soup<T>是逆变的。(不过逆变不常见)
明白了协变、稳定和逆变的观点,再看Java的完成。Java的平常泛型是稳定的,也就是说Soup<Vegetable>和Soup<Carrot>是毫无关联的两个类,不能将一个类的实例赋值给另一个类的变量。所以,上面那段用西红柿做胡萝卜汤的代码,实在基础没法经由过程编译。
二、数组协变
Java中,数组是基础范例,不是泛型,不存在Array<T>如许的东西。但它和泛型很像,都是用另一个范例构建的范例。所以,数组也是要斟酌变性的。
与泛型的稳定性差别,Java的数组是协变的。也就是说,Carrot[]是Vegetable[]的子类。而上一节中的例子已表明,协变有时会激发题目。比方下面这段代码
Vegetable[] vegetables = new Carrot[10]; vegetables[0] = new Tomato(); // 运行期毛病
由于数组是协变的,编译器许可把Carrot[10]赋值给Vegetable[]范例的变量,所以这段代码能够顺遂经由过程编译。只要在运行期,JVM真的试图往一堆胡萝卜中插进去一个西红柿的时刻,才发明大事不好。所以,上面的代码在运行期会抛出一个java.lang.ArrayStoreException范例的非常。
数组协变性,是Java的有名汗青包袱之一。运用数组时,万万要警惕!
假如把例子中的数组替换为List,状况就差别了。就像如许
ArrayList<Vegetable> vegetables = new ArrayList<Carrot>(); // 编译期毛病 vegetables.add(new Tomato());
ArrayList是一个泛型类,它是稳定的。所以,ArrayList<Carrot>和ArrayList<Vegetable>之间并没有继续关联,这段代码在编译期就会报错。
两段代码虽然都邑报错,但通常状况下,编译期毛病总比运行期毛病好处置惩罚一些。
三、当泛型也想要协变、逆变
泛型是稳定的,但某些场景里我们照样愿望它能协变起来。比方,有一个天天喝蔬菜汤减肥的小姐姐
class Girl { public void drink(Soup<Vegetable> soup) {} }
我们愿望drink要领能够接收种种差别的蔬菜汤,包含Soup<Carrot>和Soup<Tomato>。但遭到稳定性的限定,它们没法作为drink的参数。
要完成这一点,应当采纳一种类似于协变性的写法
public void drink(Soup<? extends Vegetable> soup) {}
意义是,参数soup的范例是泛型类Soup<T>,而T是Vegetable的子类(也包含Vegetable本身)。这时候,小姐姐终究能够愉快地喝上胡萝卜汤和西红柿汤了。
然则,这类要领有一个限定。编译器只晓得泛型参数是Vegetable的子类,却不晓得它细致是什么。所以,一切非null的泛型范例参数均被视为不平安的。说起来很拗口,实在很简单。直接上代码
public void drink(Soup<? extends Vegetable> soup) { soup.add(new Tomato()); // 毛病 soup.add(null); // 准确}
要领内的第一句会在编译期报错。由于编译器只晓得add要领的参数是Vegetable的子类,却不晓得它细致是Carrot、Tomato、或许其他的什么范例。这时候,通报一个细致范例的实例一概被视为不平安的。纵然soup真的是Soup<Tomato>范例也不可,由于soup的细致范例信息是在运行期才晓得的,编译期并不晓得。
然则要领内的第二句是准确的。由于参数是null,它能够是任何正当的范例。编译器以为它是平安的。
一样,也有一种类似于逆变的要领
public void drink(Soup<? super Vegetable> soup) {}
这时候,Soup<T>中的T必需是Vegetable的父类。
这类状况就不存在上面的限定了,下面的代码毫无题目
public void drink(Soup<? super Vegetable> soup) { soup.add(new Tomato()); }
Tomato是Vegetable的子类,天然也是Vegetable父类的子类。所以,编译期就能够肯定范例是平安的。
以上就是Java数组协变与范型稳定性的学问引见(附代码)的细致内容,更多请关注ki4网别的相干文章!