什么是泛型
這個概念很抽象日缨,舉個例子List<View> list = new ArrayList<>();
View就是List的泛型瞭稼,表示這個List只能存放View類型或者View子類的對象氯庆。這么說也有些籠統(tǒng)裁良,我直接把百科的介紹拿過來吧,泛型的本質(zhì)是參數(shù)化類型腾夯,也就是說所操作的數(shù)據(jù)類型被指定為一個參數(shù)颊埃,可以用在類,接口蝶俱,方法中班利。看完本篇文章你就會明白泛型到底是是什么榨呆。
泛型的好處
舉例
List arrayList = new ArrayList();
arrayList.add("aaaa");
arrayList.add(100);
for(int i = 0; i< arrayList.size();i++){
String item = (String)arrayList.get(i);
}
用了無數(shù)遍的例子罗标,這里的List沒有指定泛型,那默認(rèn)的泛型就是Object
所以這段代碼在編譯時完全沒問題积蜻。運(yùn)行程序肯定會崩潰闯割。這里存放的是Object
類型,使用的時候以String
類型使用竿拆。但是實際上添加了一個Integer類型的100宙拉,所以取出時會有類型強(qiáng)轉(zhuǎn)錯誤。
修改一下代碼
List<String> arrayList = new ArrayList();
arrayList.add("aaaa");
arrayList.add(100);
for(int i = 0; i< arrayList.size();i++){
String item = (String)arrayList.get(i);
}
好嘛丙笋,修改完編輯器直接報錯谢澈。大家應(yīng)該都知道為什么報錯煌贴。聲明了String
類型的List
卻存放了一個100
這樣把本來運(yùn)行時候發(fā)生的錯誤直接提前到編譯期,減少了代碼出錯的風(fēng)險锥忿,這就泛型的好處之一
泛型擦除
真泛型
泛型存在于編譯器和運(yùn)行期
偽泛型
泛型僅僅存在于編譯器
真泛型的代表有c#牛郑,關(guān)于真泛型不在本章類容里。學(xué)過java的人都知道java是偽泛型敬鬓,如何驗證呢淹朋?這也很簡單
java可以方法重載,重載的原則是方法名一樣列林,參數(shù)不同瑞你。
public void fun(List<Integer> integers) {
}
public void fun(List<String> strings) {
}
根據(jù)重載的原則,List<Integer>!=List<String>
重載應(yīng)該是成立的希痴。然鵝...
這里報錯了者甲,報錯信息存在里兩個相同的方法。以上的代碼寫到c#中就不會報錯砌创。
再看一個例子
List<Integer> integers = new ArrayList<>();
List<String> strings = new ArrayList<>();
if (integers.getClass().equals(strings.getClass())) {
System.out.println("類型相同");
}
以上代碼運(yùn)行會輸出日志虏缸,也可以證明java是偽泛型,泛型在運(yùn)行時會擦除嫩实。以上的 List<Integer>
List<String>
最后都會變成List
泛型的使用
泛型方法
現(xiàn)在有這樣的需求刽辙,傳入兩個對象,返回較大的對象甲献。偽代碼如下
public Object getMax(Object a, Object b) {
//……比較大小
return a;
}
public void fun() {
int a = 1;
int b = 2;
Object max = getMax(a, b);
}
傳入兩個Object對象宰缤,比較大小后返回較大的。直接寫的話返回值肯定也是Object晃洒。其實這樣很不好慨灭,在fun方法中調(diào)用時候傳入了1和2,返回的卻是個Object球及。很明顯這里返回int類型會更好氧骤。修改一下代碼
public <T> T getMax(T a, T b) {
//……比較大小
return a;
}
public void fun() {
int a = 1;
int b = 2;
int max = getMax(a, b);
}
在類上也要聲明T public class JavaTest<T>
這里使用T代表泛型,getMax方法聲明參數(shù)類型<T >
,這樣調(diào)用的時候傳入的參數(shù)是什么類型就會返回什么類型
泛型類
public class JavaTest<T> {
private T t;
public JavaTest(T t) {
this.t = t;
}
public T getT() {
return t;
}
}
//聲明JavaTest對象的時候指定泛型類型吃引,傳入的t參數(shù)的類型必須要一致
JavaTest<String> javaTest1 = new JavaTest<>("String");
JavaTest<Integer> javaTest2 = new JavaTest<>(1);
JavaTest<Boolean> javaTest3 = new JavaTest<>(true);
協(xié)變筹陵,逆變和不變
協(xié)變
先看一段代碼
public class Father {
}
public class Son extends Father{
}
Father father = new Father();
Son son = new Son();
father = son;
List<Father> fathers = new ArrayList<Son>();
Son是繼承于Father的,根據(jù)多態(tài)father = son;
這是完全沒問題的镊尺,那List<Father> fathers = new ArrayList<Son>();
應(yīng)該也沒問題吧朦佩。然鵝...
這里報錯了,因為Java的泛型是不變性質(zhì)的庐氮,也就是在List<Father>
和List<Son>
的類型不一致吕粗。Java中子類的泛型類型不屬于父類泛型類型的子類。在這個例子里就是 List<Son>
并不是List<Father>
的子類旭愧。
這種把子類的泛型對象賦值給父類的泛型的引用叫協(xié)變颅筋,因為java的泛型擦除,所以不支持協(xié)變输枯。但是實際使用中又會遇到這樣的需求议泵。
這里就要使用 通配符 ? extends
List<? extends Father> fathers = new ArrayList<Son>();
上界通配符,表示這個list是個未知的類型桃熄。extends Father 限制了未知類型的上界限先口,雖然是未知類型,但是必須是Father的子類瞳收。
所以以下的情況都是可以用的:
List<?extends Father> list1 = new ArrayList<Father>(); //本身
List<?extends Father> list2 = new ArrayList<Son>(); //直接子類
List<?extends Father> list3 = new ArrayList<Son的子類>();//間接子類
你以為這樣就沒問題了嗎
調(diào)用add方法報錯了碉京,使用? extends Father
雖然解除了協(xié)變的限制,卻又帶來了新的限制
List<? extends Father>
的泛型是個未知泛型螟深,只是限制了必須是Father的子類谐宙。所以fathers.get();
得到的肯定是fathers的。當(dāng)然也可以強(qiáng)轉(zhuǎn)成fathers的子類
使用add方法界弧,既然List<? extends Father>
是Father的子類的未知類型凡蜻,那它可能是List<Father>
也可能是List<Son>
。如果是List<Son>
的話就不能添加Father了垢箕。編輯器根本不知道List的實際類型也就無法確定add(father)
是否正確划栓。所以干脆不讓用add方法。
那這樣的協(xié)變又有什么用呢条获?忠荞??
public void fun1(List<? extends Father> list) {
for (int i = 0; i < list.size(); i++) {
list.get(i).toString();
}
}
以上的場景中fun1方法接受一個Father子類的list帅掘,然后遍歷調(diào)用toString方法委煤。這時你有個List<Son>
依然也是可以調(diào)用這個方法的。如果不使用? extends
就無法調(diào)用锄开。在遇到只需要讀取數(shù)據(jù)不修改數(shù)據(jù)數(shù)據(jù)的時候就可以使用? extends
讓java支持協(xié)變
由于這種限制素标,使用協(xié)變的泛型只能提供數(shù)據(jù)而不能修改數(shù)據(jù)。所以Java的協(xié)變是向外提供數(shù)據(jù)的一方萍悴,被稱為生產(chǎn)者 Producer
逆變
和? extends
對應(yīng)有? super
下界通配符头遭,與上界通配符對應(yīng),這里 super 限制了? 的子類癣诱。
List<? super Son> list = new ArrayList<Father>();
super限制了泛型的下界计维,必須要滿足 引用 super 對象 這個條件( son super Father
)即 后邊的泛型類型必須是前面的泛型類型的父類,正好與協(xié)變返過來撕予。
以下這些寫法都是可以的
List<? super Son> list1 = new ArrayList<Son>(); //本身
List<? super Son> list2 = new ArrayList<Father>(); //直接父類
List<? super Son> list3 = new ArrayList<Object>(); //間接父類
同樣使用? super
實現(xiàn)了逆變鲫惶,也帶來了別的限制。限制也正好與? extends
相反
List<? super Son> sons = new ArrayList<Father>();
sons.add(new Son());
Object object = sons.get(0);
同理实抡,欠母?表示未知類型欢策。這里的泛型只要是 Son 的父類就可以,所以add一個Son是可以的赏淌。
調(diào)用get方法踩寇,泛型無法確定具體的類型,只能向上取值六水,取到最大的值就是Object俺孙,如果你足夠自信的活當(dāng)然可以強(qiáng)轉(zhuǎn)成Son,實際使用上肯定存在強(qiáng)轉(zhuǎn)風(fēng)險掷贾。
那..逆變又有什么用睛榄?
public void fun2(List<? super Father> list) {
Son son = new Son();
list.add(son);
}
fun2接受一個泛型? super Father
的list的,將內(nèi)部創(chuàng)建的Son對象添加到list中想帅。
這時你有一個Father類型的List的场靴,只是想在Father類型中添加一個Son的數(shù)據(jù),根據(jù)多態(tài)的特性是完全合理的博脑,語法上就可以使用? super
來解決這個問題
Java逆變的特性確定它只能修數(shù)據(jù)不能獲取獲取,通常只拿來添加數(shù)據(jù)憎乙,往List中添加數(shù)據(jù),這種泛型類型也叫消費(fèi)者 Consumer
不變
不變是最好理解的叉趣,Java默認(rèn)的泛型就是不變類型泞边。即引用和對象并不存在什么繼承關(guān)系
小結(jié)
關(guān)于Java的協(xié)變和逆變也被總結(jié)成PECS 法則:Producer-extends, Consumer-super
說直白點就是,從數(shù)據(jù)流來看疗杉,extends是限制數(shù)據(jù)來源的(生產(chǎn)者)阵谚,而super是限制數(shù)據(jù)流入的(消費(fèi)者)。例如上面例子中烟具,使用<? extends Father> 限制存放的是Father類型的及其子類梢什,所以調(diào)用get方法一定能得到Father,但因為具體類型不明確朝聋,無法調(diào)用add方法嗡午。使用<? super Father>限制了泛型是Fathe及其父類,也就限制了add方法必須添加Father以及其父類冀痕,也因為具體類型不明確荔睹,調(diào)用get方法時候會向上取最大兼容的類型,也就是Object言蛇。
kotlin中的泛型
kotlin完全兼容java僻他,所以泛型的特點也都繼承自java。kotlin也是偽泛型腊尚,泛型的寫法也都類似吨拗,同樣也有協(xié)變和逆變的問題。
in
out
Kotlin使用關(guān)鍵字 out
來支持協(xié)變,等同于 Java 中的上界通配符 ? extends
( <? extends Father> = <out Father> )
Kotlin使用關(guān)鍵字 in
來支持逆變劝篷,等同于 Java 中的下界通配符
? super
( <? super Father> = <in Father> )
kotlin中泛型的使用:
class Producer<T> {
fun produce(): T {
...
}
}
val producer: Producer<out TextView> = Producer<Button>()
val textView: TextView = producer.produce() // 相當(dāng)于 'List' 的 `get`
class Consumer<T> {
fun consume(t: T) {
...
}
}
val consumer: Consumer<in Button> = Consumer<TextView>()
consumer.consume(Button(context)) // 相當(dāng)于 'List' 的 'add'
kotlin 泛型與java不同的地方
通配符* :
泛型中使用* 和 Java中使用通配符哨鸭?一樣
java中單獨使用?相當(dāng)于 携龟?extends Object
kotlin使用*相當(dāng)于out Any
reified 關(guān)鍵字:
在Java和Kotlin中都不能檢查一個對象是否是T類型
fathers instanceof T //java 會報錯的
100 is T //kotlin 也會報錯的
這個問題在Java中通過添加一個Class<T> 類型的參數(shù) 來解決
public<T> void check(Object item, Class<T> type) {
if (type.isInstance(item)) {
System.out.println(item);
}
}
Kotlin中也可以這么做兔跌,但是還有另外一個方法。
在inline函數(shù)中配合使用reified關(guān)鍵字
inline fun <reified T> printIfTypeMatch(item: Any) {
if (item is T) {
println(item)
}
}
類聲明處使用out和in
在類的聲明時候使用out和in峡蟋,也就定位了這個類是用來輸入還是輸出。
class Producer<out T> {
fun produce(): T {
...
}
}
val producer: Producer<TextView> = Producer<Button>() // 這里不寫 out 也不會報錯
class Consumer<in T> {
fun consume(t: T) {
...
}
}
val consumer: Consumer<Button> = Consumer<TextView>() // 這里不寫 in 也不會報錯
型變點:
在類中使用out或者in华望,型變點就是這個泛型的類型蕊蝗,也就是T
協(xié)變時,型變點只能作為返回值使用
逆變時赖舟,型變點只能作為參數(shù)使用
如果遇到協(xié)變時型變點要作為參數(shù)使用蓬戚,或者逆變時型變點要作為返回值使用”鲎ィ可以使用@UnsafeVariance解除限制(僅僅是解除編輯器報錯子漩,并不會影響協(xié)變逆變對數(shù)據(jù)讀取修改的特性)
例
interface KotlinGenericity<out T> {
fun get():T
fun add(t:@UnsafeVariance T)
}
上例中add方法中如果不使用@UnsafeVariance是會報錯的。
總結(jié)
不變類型的泛型的直接使用上應(yīng)該是沒什么難的地方石洗,主要是協(xié)變和逆變的地方幢泼。說實話我對這玩意還是有些暈,而且越想越暈讲衫。這里總結(jié)的一下關(guān)于型變的特點缕棵,大家在用的時候記住這個特點就好了。
協(xié)變:正向的繼承關(guān)系涉兽,只能讀取數(shù)據(jù)不能修改數(shù)據(jù)招驴,java中使用? extends
,kotlin中使用out
,協(xié)變的型變點只能作為返回值使用
逆變:與協(xié)變相反枷畏,逆向的繼承關(guān)系别厘,只能修改數(shù)據(jù),不能讀取數(shù)據(jù)拥诡,java中使用? super
,kotlin中使用in
触趴,逆變的型變點只能作為參數(shù)使用
不變:不存在繼承關(guān)系,既能修改數(shù)據(jù)也能讀取數(shù)據(jù)袋倔,型變點既可以當(dāng)參數(shù)也可以當(dāng)返回值
相關(guān)資料: