Java泛型解析(02):萬用字元限定,java萬用字元
Java泛型解析(02):萬用字元限定
考慮一個這樣的情境,計算數組中的最大元素。[code01]
public class ArrayUtil { public static <T> T max(T[] array) { if (array == null || 0 == array.length) { return null ;} T max = array[0]; for (int i = 1; i < array.length; i++) { if (max.compareTo(array[i]) < 0) {max = array[i];} } return max; } } 仔細看看code01裡面的代碼(代碼不完整),使用型別參數T定義一個max局部變數,這幾意味著這個max可以是任意的類型,那麼max.compareTo(array[i]) 方法的調用的前提是T所屬的類中有compareTo方法,怎麼能做到這一點呢?別著急,讓我們來看看如何給型別參數進行限定,現在來對code01中的代碼進行完善。
[code02]
public class ArrayUtil { public static <T extends Comparable<T>> T max(T[] array) { if (array == null || 0 == array.length) { return null ;} T max = array[0]; for (int i = 1; i < array.length; i++) { if (max.compareTo(array[i]) < 0) {max = array[i];} } return max; } } 注意看,我們定義型別參數的變化:<T
extends Comparable<T>>,這裡將T類型限定在Comparable及其所有的子類。是不是很好奇Comparable明明是一個interfacce,根據所學知識判斷,實現interface用的關鍵字是implements,為什麼呢?
<T
extends
Bounding Type>,表示T類型應該是綁定類型及其子類型(subType),T和綁定類型可以是類或者介面,使用
extends關鍵字因為它更接近於子類的概念,另外Java設計者並不打算為Java添加新的關鍵字如:
sub
如果給T限定多個類型,則需要使用符號"&",如下面格式
[code03]
<T extends Runnable & Serializable>
細心的讀者可能會發現,這裡限定的都是interface,如果限定為class是不是也這麼自由的呢?先不急著回答這個問題,我們知道Java中可以實現多個介面,而繼承只能是單繼承,可想而知,當我們給T限定類型的時候,限定為某個class的時候是有限制的,看看下面幾組泛型限定的代碼
[code04]
<T extends Runnable & Serializable & ArrayList> // 錯誤 <T extends Runnable & ArrayList & Serializable> // 錯誤 <T extends ArrayList & LinkedList & Serializable> // 錯誤 <T extends ArrayList & Runnable& Serializable> // 正確
不難看出,如果要限定T為class的時候,就有一個非常嚴格的規則,這個
class只能放在第一個,最多隻能有一個class。其實很容易理解,這樣一來,就能夠嚴格控制T類型是單繼承的,遵循Java的規範。
小結: 1.類型限定只能限定某個類型及其子類,使用關鍵字
extends。 2.多個型別參數用","隔開,如:<K, V>,多個限定類型用"&"隔開,如: <T
extends Runnable & Serializable> 3.限定interface的時候,對interface的個數和順序無嚴格要求,限定class時,則需要將class類型置於第一個,且最多隻能存在一個class類型。
鑽牛角尖: 問:類型限定中可以通過
extends 來限定子類型,是否可以通過類似
super關鍵字來限定超類型呢? 答:哈哈,問的好,接下來一一揭曉。 比較遺憾的是,類似<T
extends Runnable & Serializable>這樣的泛型限定子類的文法,來限定超類是沒有成為Java中的一個文法規範的,例如:[
code05]
<T super File & Runnable> // 錯誤
code03中的型別參數的定義是錯誤的,至少目前Java中沒有這樣的規範來支撐這種文法,如何解釋這個問題,筆者得花一番心思了...
不得不請教物件導向先生了: 1.
面向介面(抽象)編程,而非面向實現編程。這個設計原則告訴我們,方法調用通過高層的抽象類別或者介面來進行,具體調用的方法體就是我們實際運行時期傳遞的具體實作類別的執行個體,這也是多態的一種體現。我們實現自己的泛型是提供後期應用程式員使用的,限定一個子類,這就需要我們通過子類來調用方法,而調用的方法體則是這個類的超類的執行個體,繼承結構越往上就可能是abstract的,或者是interface,抽象類別和介面是無法執行個體化對象,這種反設計讓調用面臨失敗,一旦限定的這個類就是抽象的或者是介面,必定會造成這個泛型類或泛型方法無法使用,導致設計失敗。舉個例子:[code06]
public static <T super Runnable> void test(T runner) { runner.run(); } 這個T類型限定為Runnable的超類,這個Runnable是一個介面,無法執行個體化對象,方法參數runner就是一個不存在的執行個體,所以這是一個失敗的設計,而且這種文法也無法通過編譯器。 物件導向先生的解釋對初學者可能有點晦澀難懂,沒關係,這裡只要知道Java是不能支援這種泛型限定的。無論從設計角度,還是從後期擴充的角度,都是說不過去的。 但是不能這樣定義泛型,並不代表Java泛型中就沒有 super 關鍵字了,接下來說說泛型中的萬用字元類型,有了前面的基礎,這裡恐怕不是問題了。
萬用字元類型 萬用字元類型,相比於固定的泛型型別,它是一個巧妙的解決方案。如:[code07]
Couple<? extends Employee>
表示Couple的泛型型別是Employee或者其子類,Couple<Manager>滿足,而Couple<File>不滿足了。萬用字元用 "?" 表示。 我們來列印一下夫婦類中的wife:[code08]
public static void printWife(Couple<Employee> couple) { Employee wife = couple.getWife(); System. out.println(wife); } code08中的方法參數只能將Employee組成的夫婦傳入,貌似經理的如Couple<Manager>就不能了,這有點不合適了,搞得好像Manager還不能結婚了。所以要想讓Manager也能結婚並列印其wife,需要更改我們的設計了:[code09]
public static void printWife(Couple<? extends Employee> couple) { Employee wife = couple.getWife(); System. out .println(wife); } 萬用字元的子類型限定的文法與文章一開始介紹的類型限定有點相似,但是這裡有些細節的秘密。[code10]
public static <T extends Comparable<T>> T max(T[] array) {...} public static void printWife(Couple<? extends Employee> couple) {...} 前者T是預定義的型別參數,T可以作為一個具體的類型來定義方法的參數類型,局部變數等,T的範圍是整個方法(方法返回值,參數,方法體中局部變數),這種設計是為了給使用者帶來方便,將參數類型的指定權有限制地交給了使用者。而後者中不存在型別參數的定義,max方法參數的類型是預先定義好的Couple類型,使用者無法在使用的時候指定其他類型,但可以有限制地指定Couple中的泛型參數的類型, ?
extends Employee 自身不能單獨使用,可以理解為只能寄生在其他泛型類中,作為泛型類一個具體的型別參數,通常用於定義階段,如下面:
[
code11
]
public static ? extends Employee printWife() {...} // 錯誤 public static void printWife(? extends Empoyee employee) {...} // 錯誤 使用萬用字元來定義方法返回值和方法的參數類型在Java中是不允許的! 弄清楚了前面類型限定和萬用字元的區別以後,再引入萬用字元的超類型限定就不是那麼難以理解了。
萬用字元的超類型限定: 和前面子類型的限定一樣,用"?"表示萬用字元,它的存在必須存在泛型類的型別參數中,如:[code12]
Couple<? super Manager>
格式跟萬用字元限定子類型一樣,用了關鍵字super,但是這兩種方式的萬用字元存在一個隱形區別,讓我們來揭曉吧,先看看下面代碼:[code13]、
Couple<Manager> couple = new Couple<Manager>(new Manager(),new Manager()); Couple<? extends Employee> couple2 = couple; Employee wife = couple2.getWife(); // couple2.setWife(new Manager()); // 此處編譯錯誤
Couple<?
extends Employee>定義了couple2後可以將getWife和setWife想象成如下:[code14]
? extends Employee getWife() {...} void setWife(? extends Employee) {...} getWife是可以通過的,因為將一個返回值的引用賦給超類Employee是完全可以的,而setWife方法接受的是一個Employee的子類,具體是什麼子類,編譯器並不知道,拒絕傳遞任何特定的類型,所以couple2.setWife(
new Manager())是不能被調用的。所以萬用字元的子類限定適用於讀取。 在來看看萬用字元的超類型限定,即Couple<?
super Manager>,getWife和setWife可以想象成:[code15]
? super Manager getWife() {...} void setWife(? super Manager) {...} getWife方法的返回值是Manager的超類型,而Manger的超類型是得不到保證的,虛擬器會將它會給Object,而setWife方法是需要的是Manager的超類型,所以傳入任意Manager都是允許的,所以萬用字元的超類型限定適用於寫入。
無限定萬用字元
無限定萬用字元去除了超類型和子類型的規則,僅僅用一個"?"表示,並且也只能用於指定泛型類的型別參數中。如Couple<?>的形式,此時getWife和setWife方法如:[code16]
? getWife() {...} void setWife(?) {...} getWife返回值直接付給了Object,而setWife方法是不允許調用的。那麼既然這麼脆弱,牛逼的Java設計者為什麼還要引入這種萬用字元呢?在一些簡單的操作中,五限定萬用字元還是有用武之地的,比如:[code17]
public static boolean isCoupleComplete(Couple<?> couple) { return couple.getWife() != null && couple.getHusband() != null; } 這個方法體中,getWife和getHusband返回值都是Object類型的,此時我們只需要判斷是否為null即可,而不需要知道具體的類型是什麼,這就發揮了無限定萬用字元的作用了。發動腦經想一想,這個方法用文章開始所提到類型限定是否可以代替呢?自我思考中...[
code18]
public static <T> boolean isCoupleComplete(Couple<T> couple) { return couple.getWife() != null && couple.getHusband() != null ; } public static <T extends Employee> boolean isCoupleComplete(Couple<T> couple) { return couple.getWife() != null && couple.getHusband() != null ; } 到這裡,愛思考的讀者可能在思考一個問題,萬用字元代表了泛型類中的參數類型,在方法體中,怎麼去捕獲這個參數類型呢?這裡考慮三種萬用字元的捕獲 1.Couple<?
extends Employee> couple:getWife返回Employee 2.Couple<?
super Manager> couple:無法捕獲,getWife返回Object 3.Couple<?> couple:無法捕獲,getWife返回Object 悲催了,只有第一種能捕獲,怎麼辦呢?別著急,看看下面的小魔術:[code19]
public static void print(Couple<?> couple) { printHelp(couple); } public static <T> void printHelp(Couple<T> couple) { T husband = couple.getHusband(); T wife = couple.getWife(); couple.setHusband(wife); couple.setWife(husband); System. out.println(husband); System. out.println(wife); } 當需要捕獲萬用字元的時候,可以藉助前面所學的型別參數進行輔助,其實這是一個多餘的動作,基本上用不到這麼麻煩,這麼做是為了把萬用字元和泛型限定聯絡起來,鞏固一下之前的學習。
總結: 1.泛型參數的限定,使用extends關鍵字,限定多個類型時用"&"隔開。如:<T
extends Runnable& Serializable> 2.泛型參數限定中,如果限定的類型是class而不是interface,則class必須放在限定類表中的第一個,且最多隻能存在一個class。如:<T
extends ArrayList & Runnable& Serializable> 3.萬用字元只能用在泛型類的泛型參數中,不能單獨使用。如Couple<?>、Couple<? extends Employee> 、Couple<? super Manager> 4.萬用字元的超類型限定適用於寫入,萬用字元的子類型限定適用於讀取,無限定萬用字元適用於一些非null判斷等簡單操作。 5.萬用字元的捕獲可以藉助泛型型別限定來輔助。 這一節內容比較多,需要花點時間好好消化,體會總結中的5點,下一節,說一個深刻點的話題,虛擬機器中的針對泛型代碼的擦除。
Java泛型解析(01):認識泛型
Java泛型解析(02):萬用字元限定
Java泛型解析(03):虛擬機器執行泛型代碼
對於java泛型方法設定萬用字元的下限,看我理解是否正確
你第二種之所以不報錯,是因為方向對了,
1的E不一定能轉換成?
但是2的?一定可一轉換成R
java泛型萬用字元的問題
首先<? super B>表示的是B或者B的父類。
你的List<? super B> list = new ArrayList<>();這句賦值出現了警告是因為你將一個原生的ArraylList引用傳遞給有限制的List,改成這樣List<? super B> list = new ArrayList<A>();警告就消失了
那麼為什麼不能在其中放入A的執行個體呢,因為你將ArrayList<A>轉型為ArrayList<? super B>,編譯器會認為這個list中存放的是某一個B類的父類的執行個體,但不能確定是A,所以你傳入A會出錯,但是你傳入B或C的執行個體呢,起碼可以保證它是一個B,那就肯定沒錯了。