前面介紹各種容器之時,通過在容器名稱後面添加包裹數據類型的一對尖括弧,表示該容器存放的是哪種類型的元素。這樣一來總算把Java當中的各類括弧都湊齊了,例如包裹一段代碼的花括弧、指定數組元素下標的方括弧、容納方法輸入參數的圓括弧,還有最近跟在容器名稱之後的尖括弧。可是為什麼尖括弧要加到容器後面呢?它還 ...
前面介紹各種容器之時,通過在容器名稱後面添加包裹數據類型的一對尖括弧,表示該容器存放的是哪種類型的元素。這樣一來總算把Java當中的各類括弧都湊齊了,例如包裹一段代碼的花括弧、指定數組元素下標的方括弧、容納方法輸入參數的圓括弧,還有最近跟在容器名稱之後的尖括弧。可是為什麼尖括弧要加到容器後面呢?它還能不能用於其它場合?若想對尖括弧的來龍去脈究根問底,就得從泛型的概念說起了。
不管是方法還是類,都支持輸入指定類型的參數,其中方法的輸入參數在調用方法時填寫,而類的輸入參數可通過構造方法傳遞。在這兩種參數輸入的情況中,參數類型是早就確定好的,只有參數值才會動態變化,那要是連參數類型都不確定,得等到方法調用或者創建實例的時候才能確定參數類型,這可如何是好?為解決此種需求,各類編程語言紛紛祭出泛型的絕招,所謂“泛型”它的錶面意思是空泛的類型,也就是不明確的類型,既然類型在方法定義或者類定義時仍不明確,只好留待要用的時候再指定了。
為了更好地理解泛型的根源,接下來先看個簡單的例子。如今有兩個數字,一個是整數1,另一個是帶小數點的1.0,光光從算術方面比較的話,1與1.0肯定相等。但是到了Java語言這裡,使用包裝整型變數保存整數1,使用包裝浮點型變數保存小數1.0f,然後二者通過equals方法進行校驗,判斷結果卻是不等的。此處對整數與小數開展比較的代碼如下所示:
Integer oneInt = 1; Float oneFloat = 1.0f; boolean equalsSimple = oneInt.equals(oneFloat); System.out.println("equalsSimple="+equalsSimple);
運行以上的測試代碼,發現日誌輸出信息為“equalsSimple=false”,該結果看似咄咄怪事,其實是必然的,因為它們的變數類型都不一樣,導致編譯器認為二者的類型尚不吻合,遑論其它。若想進行包裝數值變數之間的相等判斷,就必須把有關變數轉換為相同類型,再作指定精度的數值一致性檢驗。考慮到Integer和Float都繼承自Number類型,系出同源的還有Long、Double等類型,於是可將這些包裝變數統統轉為Number類型,然後從Number變數獲取雙精度數值加以比較。據此編寫的方法代碼示例如下:
// 通過Number基類比較兩個數值變數是否相等 public static boolean equalsNumber(Number n1, Number n2) { return n1.doubleValue() == n2.doubleValue(); }
從上面的equalsNumber方法可見,它的輸入參數為Number型,同時涵蓋了Number及其派生出來的所有子類。對於這種情況,Java允許泛化參數類型,即先聲明一個由Number擴展而來的類型T,再把T作為輸入參數的變數類型。下麵是具體的類型泛化代碼:
// 通過泛型變數比較兩個數值變數是否相等。利用尖括弧包裹泛型的派生操作 public static <T extends Number> boolean equalsGeneric(T t1, T t2) { return t1.doubleValue() == t2.doubleValue(); }
雖然equalsNumber與equalsGeneric的參數格式有所不同,但實際上兩個方法是等價的,它們支持的入參類型都屬於Number及其子類。
緊接著再來看個數組元素拼接成字元串的例子,編碼調試的過程中,程式員常常想知道某個數組裡面究竟放了哪些元素,這時便需要將數組的所有元素都列印出來。然而數組變數自身不能自動轉成字元串,只能通過Arrays工具的toString方法輸出拼接好的字元串,倘若由程式員自己編碼去拼接數組元素,那又該如何處理?因為普通的數據類型也同時支持數組形式,所以要想整個通用的字元串拼接方法,必須找到這些數據類型的共同基類,恰好Java也提供了這個基類名叫Object,那末把“Object[]”當作通用的數組類型真是再合適不過了。如此一來,數組各元素的字元串拼接代碼就變成了下麵這般:
// 把對象數組裡的各個元素拼接成字元串 public static String objectsToString(Object[] array) { String result = ""; if (array!=null && array.length>0) { for (int i=0; i<array.length; i++) { if (i > 0) { result = result + " | "; } result = result + array[i].toString(); } } return result; }
接著讓外部運行一段測試代碼,檢查看看字元串拼接是否正常運行,測試代碼如下:
Double[] doubleArray = new Double[] { 1.1, 2D, 3.1415926, 11.11 }; System.out.println("objectsToString=" + objectsToString(doubleArray));
運行上述的測試代碼,觀察輸出的日誌發現拼接功能完全正常:
objectsToString=1.1 | 2.0 | 3.1415926 | 11.11
Object作為普通數據類型的基類,自然它也支持泛化的寫法,即先聲明一個由Object擴展而來的類型T,再把T作為輸入參數的變數類型,於是類型泛化的代碼格式形如“<T extends Object>”。由於Object是Java預設的原始基類,如同大家自定義新類時都沒寫“extends Object”那樣,類型泛化也不必顯式寫明“extends Object”,因此“<T extends Object>”完成可以簡寫為“<T>”。這樣採取泛化簡寫的字元串拼接泛型代碼如下所示:
// 把泛型數組裡的各個元素拼接成字元串。<T> 等同於 <T extends Object> //public static <T extends Object> String arraysToString(T[] array) { public static <T> String arraysToString(T[] array) { String result = ""; if (array!=null && array.length>0) { for (int i=0; i<array.length; i++) { if (i > 0) { result = result + " | "; } result = result + array[i].toString(); } } return result; }
現在給出了數組類型的泛型寫法,容器類型也能依樣畫葫蘆,對應於泛型數組的“T[]”,原先通用的清單數據就變成了類型“List<T>”。改寫之後的清單元素拼接代碼示例如下:
// 把List清單里的各個元素拼接成字元串,此處使用了泛型 public static <T> String listToString(List<T> list) { String result = ""; if (list!=null && list.size()>0) { for (int i=0; i<list.size(); i++) { if (i > 0) { result = result + " | "; } result = result + list.get(i).toString(); } } return result; }
對於包括清單在內的容器類型來說,還能在尖括弧內部填上問號,同樣表示裡面的數據類型是不確定的,就像下列代碼演示的那樣:
// 把List清單里的各個元素拼接成字元串,此處使用了問號表示不確定類型 public static String listToStringByQuestion(List<?> list) { String result = ""; if (list!=null && list.size()>0) { for (int i=0; i<list.size(); i++) { if (i > 0) { result = result + " | "; } result = result + list.get(i).toString(); } } return result; }
不過帶有問號的“<?>”寫法有很大的局限性,它既不如泛型靈活,也不如Object通用。問號寫法僅僅適用於個別場合,並不推薦在一般方法中運用。單單拿問號跟泛型比較的話,主要有以下幾點區別:
1、問號只能用於給泛型類創建實例,本身不能創建實例。而泛型T既可用於泛型類創建實例,也可用於給自身創建實例,如“T t;”
2、問號只可用作輸入參數,不可用作輸出參數。而泛型T用於二者皆可。
3、使用了問號的容器實例,只允許調用get方法,不允許調用add方法。而泛型容器不存在方法調用的限制。
更多Java技術文章參見《Java開發筆記(序)章節目錄》