淺談java的泛型

什么是泛型

這個概念很抽象日缨,舉個例子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

image

這樣把本來運(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)該是成立的希痴。然鵝...

image

這里報錯了者甲,報錯信息存在里兩個相同的方法。以上的代碼寫到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)該也沒問題吧朦佩。然鵝...

image

這里報錯了,因為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的子類>();//間接子類

你以為這樣就沒問題了嗎

image

調(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是會報錯的。

image

總結(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)資料:

Kotlin 的泛型
java 泛型詳解-絕對是對泛型方法講解最詳細(xì)的
協(xié)變與逆變

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末雕蔽,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子宾娜,更是在濱河造成了極大的恐慌批狐,老刑警劉巖,帶你破解...
    沈念sama閱讀 216,324評論 6 498
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異嚣艇,居然都是意外死亡承冰,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,356評論 3 392
  • 文/潘曉璐 我一進(jìn)店門食零,熙熙樓的掌柜王于貴愁眉苦臉地迎上來困乒,“玉大人,你說我怎么就攤上這事贰谣∧嚷В” “怎么了?”我有些...
    開封第一講書人閱讀 162,328評論 0 353
  • 文/不壞的土叔 我叫張陵吱抚,是天一觀的道長百宇。 經(jīng)常有香客問我,道長秘豹,這世上最難降的妖魔是什么携御? 我笑而不...
    開封第一講書人閱讀 58,147評論 1 292
  • 正文 為了忘掉前任,我火速辦了婚禮既绕,結(jié)果婚禮上啄刹,老公的妹妹穿的比我還像新娘。我一直安慰自己凄贩,他們只是感情好誓军,可當(dāng)我...
    茶點故事閱讀 67,160評論 6 388
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著怎炊,像睡著了一般谭企。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上评肆,一...
    開封第一講書人閱讀 51,115評論 1 296
  • 那天债查,我揣著相機(jī)與錄音,去河邊找鬼瓜挽。 笑死盹廷,一個胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的久橙。 我是一名探鬼主播俄占,決...
    沈念sama閱讀 40,025評論 3 417
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼淆衷!你這毒婦竟也來了缸榄?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 38,867評論 0 274
  • 序言:老撾萬榮一對情侶失蹤祝拯,失蹤者是張志新(化名)和其女友劉穎甚带,沒想到半個月后她肯,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,307評論 1 310
  • 正文 獨居荒郊野嶺守林人離奇死亡鹰贵,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,528評論 2 332
  • 正文 我和宋清朗相戀三年晴氨,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片碉输。...
    茶點故事閱讀 39,688評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡籽前,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出敷钾,到底是詐尸還是另有隱情枝哄,我是刑警寧澤,帶...
    沈念sama閱讀 35,409評論 5 343
  • 正文 年R本政府宣布闰非,位于F島的核電站膘格,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏财松。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 41,001評論 3 325
  • 文/蒙蒙 一纱控、第九天 我趴在偏房一處隱蔽的房頂上張望辆毡。 院中可真熱鬧,春花似錦甜害、人聲如沸舶掖。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,657評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽眨攘。三九已至,卻和暖如春嚣州,著一層夾襖步出監(jiān)牢的瞬間鲫售,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 32,811評論 1 268
  • 我被黑心中介騙來泰國打工该肴, 沒想到剛下飛機(jī)就差點兒被人妖公主榨干…… 1. 我叫王不留情竹,地道東北人。 一個月前我還...
    沈念sama閱讀 47,685評論 2 368
  • 正文 我出身青樓匀哄,卻偏偏與公主長得像秦效,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子涎嚼,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 44,573評論 2 353

推薦閱讀更多精彩內(nèi)容