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