前語(yǔ):不要為了讀文章而讀文章,一定要帶著問(wèn)題來(lái)讀文章,勤思考。 來(lái)源:http://ick/G2m 問(wèn):Java 的泛型是什么?有什么好處和優(yōu)點(diǎn)?JDK 不同版本的泛型有什么區(qū)別?答: 泛型是 Java SE 1.5 的新特性,泛型的本質(zhì)是參數(shù)化類(lèi)型,這種參數(shù)類(lèi)型可以用在類(lèi)、接口和方法的創(chuàng)建中,分別稱(chēng)為泛型類(lèi)、泛型接口、泛型方法。在 Java SE 1.5 之前沒(méi)有泛型的情況的下只能通過(guò)對(duì)類(lèi)型 Object 的引用來(lái)實(shí)現(xiàn)參數(shù)的任意化,其帶來(lái)的缺點(diǎn)是要做顯式強(qiáng)制類(lèi)型轉(zhuǎn)換,而這種強(qiáng)制轉(zhuǎn)換編譯期是不做檢查的,容易把問(wèn)題留到運(yùn)行時(shí),所以 泛型的好處是在編譯時(shí)檢查類(lèi)型安全,并且所有的強(qiáng)制轉(zhuǎn)換都是自動(dòng)和隱式的,提高了代碼的重用率,避免在運(yùn)行時(shí)出現(xiàn) ClassCastException。 JDK 1.5 引入了泛型來(lái)允許強(qiáng)類(lèi)型在編譯時(shí)進(jìn)行類(lèi)型檢查;JDK 1.7 泛型實(shí)例化類(lèi)型具備了自動(dòng)推斷能力,譬如 List 問(wèn):Java 泛型是如何工作的?什么是類(lèi)型擦除?答: 泛型是通過(guò)類(lèi)型擦除來(lái)實(shí)現(xiàn)的,編譯器在編譯時(shí)擦除了所有泛型類(lèi)型相關(guān)的信息,所以在運(yùn)行時(shí)不存在任何泛型類(lèi)型相關(guān)的信息,譬如 List 問(wèn):Java 泛型類(lèi)、泛型接口、泛型方法有什么區(qū)別?答: 泛型類(lèi)是在實(shí)例化類(lèi)的對(duì)象時(shí)才能確定的類(lèi)型,其定義譬如 class Test 泛型接口與泛型類(lèi)一樣,其定義譬如 interface Generator 泛型方法所在的類(lèi)可以是泛型類(lèi)也可以是非泛型類(lèi),是否擁有泛型方法與所在的類(lèi)無(wú)關(guān),所以在我們應(yīng)用中應(yīng)該盡可能使用泛型方法,不要放大作用空間,尤其是在 static 方法時(shí) static 方法無(wú)法訪(fǎng)問(wèn)泛型類(lèi)的類(lèi)型參數(shù),所以更應(yīng)該使用泛型的 static 方法(聲明泛型一定要寫(xiě)在 static 后返回值類(lèi)型前)。泛型方法的定義譬如 問(wèn):Java 如何優(yōu)雅的實(shí)現(xiàn)元組?答: 元組其實(shí)是關(guān)系數(shù)據(jù)庫(kù)中的一個(gè)學(xué)術(shù)名詞,一條記錄就是一個(gè)元組,一個(gè)表就是一個(gè)關(guān)系,紀(jì)錄組成表,元組生成關(guān)系,這就是關(guān)系數(shù)據(jù)庫(kù)的核心理念。很多語(yǔ)言天生支持元組,譬如 Python 等,在語(yǔ)法本身支持元組的語(yǔ)言中元組是用括號(hào)表示的,如 (int, bool, string) 就是一個(gè)三元組類(lèi)型,不過(guò)在 Java、C 等語(yǔ)言中就比較坑爹,語(yǔ)言語(yǔ)法本身不具備這個(gè)特性,所以在 Java 中我們?nèi)绻雰?yōu)雅實(shí)現(xiàn)元組就可以借助泛型類(lèi)實(shí)現(xiàn),如下是一個(gè)三元組類(lèi)型的實(shí)現(xiàn): 問(wèn):下面程序塊的運(yùn)行結(jié)果是什么,為什么? 答: 上面代碼段結(jié)果為 true,解釋如下。 因?yàn)?load 的是同一個(gè) class 文件,存在 ArrayList.class 文件但是不存在 ArrayList 問(wèn):為什么 Java 泛型要通過(guò)擦除來(lái)實(shí)現(xiàn)?擦除有什么壞處或者說(shuō)代價(jià)?答: 可以說(shuō) Java 泛型的存在就是一個(gè)不得已的妥協(xié),正因?yàn)檫@種妥協(xié)導(dǎo)致了 Java 泛型的混亂,甚至說(shuō)是 JDK 泛型設(shè)計(jì)的失敗。Java 之所以要通過(guò)擦除來(lái)實(shí)現(xiàn)泛型機(jī)制其實(shí)是為了兼容性考慮,只有這樣才能讓非泛化代碼到泛化代碼的轉(zhuǎn)變過(guò)程建立在不破壞現(xiàn)有類(lèi)庫(kù)的實(shí)現(xiàn)上。正是因?yàn)檫@種兼容也帶來(lái)了一些代價(jià),譬如泛型不能顯式地引用運(yùn)行時(shí)類(lèi)型的操作之中(如向上向下轉(zhuǎn)型、instanceof 操作等),因?yàn)樗嘘P(guān)于參數(shù)的信息都丟失了,所以任何時(shí)候使用泛型都要提醒自己背后的真實(shí)擦除類(lèi)型到底是什么;此外擦除和兼容性導(dǎo)致了使用泛型并不是強(qiáng)制的(如 List 問(wèn):下面三個(gè) funcX 方法有問(wèn)題嗎,為什么?答:func1、func2、func3 三個(gè)方法均無(wú)法編譯通過(guò)。 因?yàn)榉盒筒脸齺G失了在泛型代碼中執(zhí)行某些操作的能力,任何在運(yùn)行時(shí)需要知道確切類(lèi)型信息的操作都將無(wú)法工作。 問(wèn):下面代碼段有問(wèn)題嗎,運(yùn)行效果是什么,為什么? 答: 由于在程序中定義的 ArrayList 泛型類(lèi)型實(shí)例化為 Integer 的對(duì)象,如果直接調(diào)用 add 方法則只能存儲(chǔ)整形數(shù)據(jù),不過(guò)當(dāng)我們利用反射調(diào)用 add 方法時(shí)就可以存儲(chǔ)字符串,因?yàn)?Integer 泛型實(shí)例在編譯之后被擦除了,只保留了原始類(lèi)型 Object,所以自然可以插入。 問(wèn):請(qǐng)比較深入的談?wù)勀銓?duì) Java 泛型擦除的理解和帶來(lái)的問(wèn)題認(rèn)識(shí)?答:Java 的泛型是偽泛型,因?yàn)樵诰幾g期間所有的泛型信息都會(huì)被擦除掉,譬如 List 先檢查再擦除的類(lèi)型檢查是針對(duì)引用的,用引用調(diào)用泛型方法就會(huì)對(duì)這個(gè)引用調(diào)用的方法進(jìn)行類(lèi)型檢測(cè)而無(wú)關(guān)它真正引用的對(duì)象。可以說(shuō)這是為了兼容帶來(lái)的問(wèn)題,如下: 所以說(shuō)擦除前的類(lèi)型檢查是針對(duì)引用的,用這個(gè)引用調(diào)用泛型方法就會(huì)對(duì)這個(gè)引用調(diào)用的方法進(jìn)行類(lèi)型檢測(cè)而無(wú)關(guān)它真正引用的對(duì)象。 先檢查再擦除帶來(lái)的另一個(gè)問(wèn)題就是泛型中參數(shù)化類(lèi)型無(wú)法支持繼承關(guān)系,因?yàn)榉盒偷脑O(shè)計(jì)初衷就是為了解決 Object 類(lèi)型轉(zhuǎn)換的弊端而存在,如果泛型中參數(shù)化類(lèi)型支持繼承操作就違背了設(shè)計(jì)的初衷而繼續(xù)回到原始的 Object 類(lèi)型轉(zhuǎn)換弊端。也同樣可以說(shuō)這是為了兼容帶來(lái)的問(wèn)題,如下: 之所以這樣我們可以從反面來(lái)論證,假設(shè)編譯不報(bào)錯(cuò)則當(dāng)通過(guò) arrayList2 調(diào)用 get() 方法取值時(shí)返回的是 String 類(lèi)型的對(duì)象(因?yàn)轭?lèi)型檢測(cè)是根據(jù)引用來(lái)決定的),而實(shí)際上存放的是 Object 類(lèi)型的對(duì)象,這樣 get 出來(lái)就會(huì) ClassCastException 了,所以這違背了泛型的初衷。對(duì)于 arrayList4 同樣假設(shè)編譯不報(bào)錯(cuò),當(dāng)調(diào)用 arrayList4 的 get() 方法取出來(lái)的 String 變成了 Object 雖然不會(huì)出現(xiàn) ClassCastException,但是依然沒(méi)有意義啊,泛型出現(xiàn)的原因就是為了解決類(lèi)型轉(zhuǎn)換的問(wèn)題,其次如果我們通過(guò) arrayList4 的 add() 方法繼續(xù)添加對(duì)象則可以添加任意類(lèi)型對(duì)象實(shí)例,這就會(huì)導(dǎo)致我們 get() 時(shí)更加懵逼不知道加的是什么類(lèi)型了,所以怎么說(shuō)都是個(gè)死循環(huán)。 擦除帶來(lái)的另一個(gè)問(wèn)題就是泛型與多態(tài)的沖突,其通過(guò)子類(lèi)中生成橋方法解決了多態(tài)沖突問(wèn)題,這個(gè)問(wèn)題的驗(yàn)證也很簡(jiǎn)單,可以通過(guò)下面的例子說(shuō)明: 上面代碼段的運(yùn)行情況很詫異吧,按理來(lái)說(shuō) Creater 類(lèi)被編譯擦除后 setValue 方法的參數(shù)應(yīng)該是 Object 類(lèi)型了,子類(lèi) StringCreater 的 setValue 方法參數(shù)類(lèi)型為 String,看起來(lái)父子類(lèi)的這組方法應(yīng)該是重載關(guān)系,所以調(diào)用子類(lèi)的 setValue 方法添加字符串和 Object 類(lèi)型參數(shù)應(yīng)該都是合法才對(duì),然而從編譯來(lái)看子類(lèi)根本沒(méi)有繼承自父類(lèi)參數(shù)為 Object 類(lèi)型的 setValue 方法,所以說(shuō)子類(lèi)的 setValue 方法是對(duì)父類(lèi)的重寫(xiě)而不是重載(從子類(lèi)添加 @Override 注解沒(méi)報(bào)錯(cuò)也能說(shuō)明是重寫(xiě)關(guān)系)。關(guān)于出現(xiàn)上面現(xiàn)象的原理其實(shí)我們通過(guò) javap 看下兩個(gè)類(lèi)編譯后的本質(zhì)即可: 通過(guò)編譯后的字節(jié)碼我們可以看到 Creater 泛型類(lèi)在編譯后類(lèi)型被擦除為 Object,而我們子類(lèi)的本意是進(jìn)行重寫(xiě)實(shí)現(xiàn)多態(tài),可類(lèi)型擦除后子類(lèi)就和多態(tài)產(chǎn)生了沖突,所以編譯后的字節(jié)碼里就出現(xiàn)了橋方法來(lái)實(shí)現(xiàn)多態(tài)。可以看到橋方法的參數(shù)類(lèi)型都是 Object,也就是說(shuō)子類(lèi)中真正覆蓋父類(lèi)方法的是橋方法,而子類(lèi) String 參數(shù) setValue、getValue 方法上的 @Oveerride 注解只是個(gè)假象,橋方法的內(nèi)部實(shí)現(xiàn)是直接調(diào)用了我們自己重寫(xiě)的那兩個(gè)方法;不過(guò)上面的 setValue 方法是為了解決類(lèi)型擦除與多態(tài)之間的沖突生成的橋方法,而 getValue 是一種協(xié)變,之所以子類(lèi)中 Object getValue() 和 String getValue() 方法可以同時(shí)存在是虛擬機(jī)內(nèi)部的一種區(qū)分(我們自己寫(xiě)的代碼是不允許這樣的),因?yàn)樘摂M機(jī)內(nèi)部是通過(guò)參數(shù)類(lèi)型和返回類(lèi)型來(lái)確定一個(gè)方法簽名的,所以編譯器為了實(shí)現(xiàn)泛型的多態(tài)允許自己做這個(gè)看起來(lái)不合法的實(shí)現(xiàn),實(shí)質(zhì)還是交給了虛擬機(jī)去區(qū)別。 先檢查再擦除帶來(lái)的另一個(gè)問(wèn)題就是泛型讀取時(shí)會(huì)進(jìn)行自動(dòng)類(lèi)型轉(zhuǎn)換問(wèn)題,所以如果調(diào)用泛型方法的返回類(lèi)型被擦除則在調(diào)用該方法時(shí)插入強(qiáng)制類(lèi)型轉(zhuǎn)換。 擦除帶來(lái)的另一個(gè)問(wèn)題是泛型類(lèi)型參數(shù)不能是基本類(lèi)型,比如 ArrayList 擦除帶來(lái)的另一個(gè)問(wèn)題是無(wú)法進(jìn)行具體泛型參數(shù)類(lèi)型的運(yùn)行時(shí)類(lèi)型檢查,譬如 arrayList instanceof ArrayList 擦除帶來(lái)的另一個(gè)問(wèn)題是我們不能拋出也不能捕獲泛型類(lèi)的對(duì)象,因?yàn)楫惓J窃谶\(yùn)行時(shí)捕獲和拋出的,而在編譯時(shí)泛型信息會(huì)被擦除掉,擦除后兩個(gè) catch 會(huì)變成一樣的東西。也不能在 catch 子句中使用泛型變量,因?yàn)榉盒托畔⒃诰幾g時(shí)已經(jīng)替換為原始類(lèi)型(譬如 catch(T) 在限定符情況下會(huì)變?yōu)樵碱?lèi)型 Throwable),如果可以在 catch 子句中使用則違背了異常的捕獲優(yōu)先級(jí)順序。 問(wèn):為什么 Java 的泛型數(shù)組不能采用具體的泛型類(lèi)型進(jìn)行初始化?答: 這個(gè)問(wèn)題可以通過(guò)一個(gè)例子來(lái)說(shuō)明。 由于 JVM 泛型的擦除機(jī)制,所以上面代碼可以給 oa[1] 賦值為 ArrayList 也不會(huì)出現(xiàn)異常,但是在取出數(shù)據(jù)的時(shí)候卻要做一次類(lèi)型轉(zhuǎn)換,所以就會(huì)出現(xiàn) ClassCastException,如果可以進(jìn)行泛型數(shù)組的聲明則上面說(shuō)的這種情況在編譯期不會(huì)出現(xiàn)任何警告和錯(cuò)誤,只有在運(yùn)行時(shí)才會(huì)出錯(cuò),但是泛型的出現(xiàn)就是為了消滅 ClassCastException,所以如果 Java 支持泛型數(shù)組初始化操作就是搬起石頭砸自己的腳。而對(duì)于下面的代碼來(lái)說(shuō)是成立的: 所以說(shuō)采用通配符的方式初始化泛型數(shù)組是允許的,因?yàn)閷?duì)于通配符的方式最后取出數(shù)據(jù)是要做顯式類(lèi)型轉(zhuǎn)換的,符合預(yù)期邏輯。綜述就是說(shuō)Java 的泛型數(shù)組初始化時(shí)數(shù)組類(lèi)型不能是具體的泛型類(lèi)型,只能是通配符的形式,因?yàn)榫唧w類(lèi)型會(huì)導(dǎo)致可存入任意類(lèi)型對(duì)象,在取出時(shí)會(huì)發(fā)生類(lèi)型轉(zhuǎn)換異常,會(huì)與泛型的設(shè)計(jì)思想沖突,而通配符形式本來(lái)就需要自己強(qiáng)轉(zhuǎn),符合預(yù)期。 關(guān)于這道題的答案其 Oracle 官方文檔給出了原因:https://docs.oracle.com/javase/tutorial/extra/generics/fineprint.html 問(wèn):下面語(yǔ)句哪些是有問(wèn)題,哪些沒(méi)有問(wèn)題? 答: 上面每個(gè)語(yǔ)句的問(wèn)題注釋部分已經(jīng)闡明了,因?yàn)樵?Java 中是不能創(chuàng)建一個(gè)確切的泛型類(lèi)型的數(shù)組的,除非是采用通配符的方式且要做顯式類(lèi)型轉(zhuǎn)換才可以。問(wèn):如何正確的初始化泛型數(shù)組實(shí)例?答: 這個(gè)無(wú)論我們通過(guò) new ArrayList[10] 的形式還是通過(guò)泛型通配符的形式初始化泛型數(shù)組實(shí)例都是存在警告的,也就是說(shuō)僅僅語(yǔ)法合格,運(yùn)行時(shí)潛在的風(fēng)險(xiǎn)需要我們自己來(lái)承擔(dān),因此那些方式初始化泛型數(shù)組都不是最優(yōu)雅的方式,我們?cè)谑褂玫椒盒蛿?shù)組的場(chǎng)景下應(yīng)該盡量使用列表集合替換,此外也可以通過(guò)使用 java.lang.reflect.Array.newInstance(Class 所以使用反射來(lái)初始化泛型數(shù)組算是優(yōu)雅實(shí)現(xiàn),因?yàn)榉盒皖?lèi)型 T 在運(yùn)行時(shí)才能被確定下來(lái),我們能創(chuàng)建泛型數(shù)組也必然是在 Java 運(yùn)行時(shí)想辦法,而運(yùn)行時(shí)能起作用的技術(shù)最好的就是反射了。 問(wèn):Java 泛型對(duì)象能實(shí)例化 T t = new T() 嗎,為什么?答: 不能,因?yàn)樵?Java 編譯期沒(méi)法確定泛型參數(shù)化類(lèi)型,也就找不到對(duì)應(yīng)的類(lèi)字節(jié)碼文件,所以自然就不行了,此外由于 T 被擦除為 Object,如果可以 new T() 則就變成了 new Object(),失去了本意。如果要實(shí)例化一個(gè)泛型 T 則可以通過(guò)反射實(shí)現(xiàn)(實(shí)例化泛型數(shù)組也類(lèi)似),如下: |
|