步步理解 JAVA 泛型编程 – 共三篇
FROM: http://unmi.cc/understand-java-generic-3
步步理解 JAVA 泛型編程 – 共三篇
原作者:Unmi(1125535)
第一篇:
JDK 1.5 相對于 JDK 1.4 來說變化最大的部分就是泛型,甚至可以說自 Java 1.0 發布以來也是最大的一次語言變化,因為要涉及到編譯器的大改動。很早的時候大家對泛型的呼聲很大,正如 C++ 的模板,C# 的泛型確實是個值得借鑒的好特性。JDK1.5 這前,很多人對于泛型是急不可耐,在 JDK1.4 下搭配一個外掛的 Generic Java 編譯器,通老實 -Xbootclasspath、-bootclasspath 同樣能讓你在 1.4 的 JDK 上使用泛型:
javac -J-Xbootclasspath/p:JSR14HOME\gjcrt.jar -bootclasspath JSR14HOME\collect.jar;JDK141HOME\jre\lib\rt.jar -source 1.5 FileName.java java -Xbootclasspath/p:JSR14HOME\gjc-rt.jar FileName
JDK 1.5 雖說出來這么久了,JDK 7 眼看都要見得天日了,不過可能我們很多人也只是知道怎么使用支持泛型的集合,也許還沒有寫過自己的泛型類,更不用說泛型的一些更高階規范了。正如我們會在 Hibernate、Spring 里很熟練的使用零配置的注解,好像也很少有機會讓我們去寫處理注解的代碼似的。因為畢竟我們不是書寫框架的,多數時候是在應用它們。
多看JDK 的源代碼固然是個好辦法,但除看之外,練手幫助理解是必不可少的。如果你熟悉 C++,可以與 C++ 的模板類比。現在來一步步了解泛型。首先泛型這一稱謂聽來有點抽象,換作參數化類型,或者借用模板類的叫法會好理解的多。
比如說這樣一個泛型類:
package com.unmi;
public class Demo<T> {
private T first;
public T getFirst() {
return first;
}
public void setFirst(T first) {
this.first = first;
}
}
聲明部分 public class Demo<T> 為 Demo 類指定了一個類型參數 T,那么在該方法就可以使用該類型 T,如作為實例、局部變量,方法參數或返回值。
實際使用該類時 T 要被替換為具體的類型,如類型為 String,用 Demo<String> demo= new Demo<String>() 聲明構造,那么上面的變量及方法的原型分別是:
String first; String getFirst(); void setFirst(String first);
如果具體類型為 Integer,用 Demo<Integer> demo = new Demo<Integer>() 聲明構造,則上面的變量及方法的原型分別是:
Integer first; Integer getFirst(); void setFirst(Integer first);
可以是任何的具體類型,所以可以理解為泛型類是普通類的工廠。其實依照 C++ 的名詞,前面類的定義稱之為一個模板類,使用的時候是讓模板中的類型具體化,這樣好理解些。但要記得,Java 的泛型與 C++ 的模板實現的泛型內部機制還是有本質區別的。
前面是在類的聲明部分定義了一個類型參數,如果有如個類型參數也是可以的,寫成
public class Demo<T,U>,多少個都行 public class Demo<T,U,S>,在類的變量或方法中可以使聲明部分定義的類型。
類型用大寫形式,一般都是用 T 指代 Type,K 指代 Key,V 指代 Value,其他靠著 T 的 U,S ...... 都隨意用了。
不妨來看個 C++ 的模板類,以及使用代碼:
//類聲明前要加個模板聲明打頭
template <class Type> class Stack{
public:
Type Push(Type item);
};
//每個方法也都要以模板聲明打頭
template<class Type> Type Stack<Type>::Push(Type item){
return item;
}
int main(int argc, char* argv[]){
Stack<int> fishes;
cout<<fishes.Push(123)<<endl;
return 0;
}
參考:1. Using Java Generics
本篇鏈接 http://unmi.cc/understand-java-generic-1
第二篇:
前面講了泛型類的定義,你也可以在一個普通類中單單定義一個泛型方法。也就是說類能夠帶個類型參數,方法也可以帶類型參數的。還是來看個例子(包括如何應用),一個獲得數組中間元素的方法,因為數組中存儲元素的類型是不定的,所以把該方法定義成泛型的。
package com.unmi;
/**
* 泛型方法示例
* @author Unmi
*/
public class ArrayAlg {
//這個就是在普通類 ArrayAlg 中定義的泛型方法
public static <T> T getMiddle(T[] a){
return a[a.length/2];
}
public static void main(String[] args) {
String[] names = {"Fantasia","Unmi","Kypfos"};
//String middle = ArrayAlg.<String>getMiddle(names);
//上面那樣寫是可以,編譯器可推斷出要調用的方法,所以省去<String>
String middle = ArrayAlg.getMiddle(names);
System.out.println(middle);
}
}
我們之所以說上面的 ArrayAlg 是個普通類,是因為沒有在類聲明部分引入類型參數(比如聲明為 public class ArrayAlg<T>)。同時在理解上面的泛型方法 getMiddel() 時應聯想到泛型類是如何定義的。
對比前面泛型類的定義 public class Demo<T>{.......},那么在類的變量、方法參數、返回值等處就可以使用參數類型 T。 這里定義了泛型方法 public static <T> T getMiddle(T[] a){......},同樣是用 <T> 的形式為方法引入了一個類型參數,那么這個類型 T 可用作該方法的返回值、參數、或局部變量。注意這里的 <T> T,前部分 <T> 是定義泛型方法的類型參數,后部 T 是該方法的返回值。
泛型類的類型參數(<T>) 是緊貼著類名的后面,而泛型方法的類型參數(<T>) 是緊貼著方法聲明的返回類型的前面。
我們在使用泛型類,也是在構造的時候類緊貼類名后加上具體的參數類型,如 Demo<String> demo = new Demo<String>();類似的,我們在使用泛型方法時,從代碼語法是在緊貼方法名的前面加代換上具體的參數類型,如 ArrayAlg.<String>getMiddle(names),調用方法時不能有返回類型了,所以具體參數類型 <String> 靠緊了方法名。
前面代碼中,我們說既可以用 ArrayAlg.<String>getMiddle(names); 來調用定義的泛型方法 public static <T> T getMiddle(T[] a),也可省寫為 ArrayAlg.getMiddle(names); 來調用該方法。通常我們是這么做的,原因是 Java 編譯器通過參數類型、個數等信息能推斷出調用哪一個方法。但 Java 編譯器也不是完全可靠的,有時候你必須顯式的用 ArrayAlg.<String>getMiddle(names); 這種形式去調用明確的方法。
例如,我們在 ArrayAlg 中多定義一個 public static String getMiddle(String[] a){......} 方法,完整代碼如下:
package com.unmi;
/**
* 泛型方法示例,泛型方法的顯式調用
* @author Unmi
*/
public class ArrayAlg {
//這個就是在普通類 ArrayAlg 中定義的泛型方法
public static <T> T getMiddle(T[] a){
return a[a.length/2];
}
public static String getMiddle(String[] a){
return "Not Generic Method.";
}
public static void main(String[] args) {
String[] names = {"Fantasia","Unmi","Kypfos"};
//必須顯式的用 <String> 去調用定義的泛型方法
String middle1 = ArrayAlg.<String>getMiddle(names);
System.out.println(middle1); //輸入 Unmi,調用了泛型方法
//不指明參數類型 <String> 則調用的是那個普通方法
String middle2 = ArrayAlg.getMiddle(names);
System.out.println(middle2); //輸出 Not Generic Method
}
}
這也有些像我們的 C++ 的模板類,在模板具體化的時候存在 隱式實例化、顯式實例化、顯式具體化、部分具體化的情況,怎么看 C++ 的模板類還是要比 Java 的泛型復雜。
當然,上面代碼只是說明 Java 的泛型方法在語法上會出現這種情況,倘若誰真寫出的泛型代碼需要用 ArrayAlg.<String>getMiddle(names); 顯式的去調用泛型方法,那一定要考慮重構它了。明白了這一點難道就沒有半點實際的意義嗎,自然也不是,我們可以把它牢記為潛在的 Bug 容身之所。
進一步聯系到前一篇,泛型類在定義的時候可以指定多個類型參數(用 <T,U> 形式),在定義泛型方法時同樣用 <T,U> 的形式,調用的時候與一個參數時類似,如 ArrayAlg.<String, Date>getByIdx(names, new Date())。也不怕浪費幾個字,大致瀏覽一下多類型參數時泛型方法的定義與使用的代碼:
package com.unmi;
import java.util.*;
/**
* 泛型方法示例,多類型參數的情況
* @author Unmi
*/
public class ArrayAlg {
//由索引獲得
public static <T,U> T getByIdx(T[] a, U b){
//依照 HashMap 實現的算法,由 b 得到一個不越界的索引
int h = b.hashCode();
h ^= (h >>> 20) ^ (h >>> 12);
h = h ^ (h >>> 7) ^ (h >>> 4);
int index = h & (a.length-1);
return a[index];
}
public static void main(String[] args) {
String[] names = {"Fantasia","Kypfos","Unmi"};
//顯式的用 <String, Object> 去調用定義的泛型方法
String name = ArrayAlg.<String, Date>getByIdx(names,new Date());
//隱式調用泛型方法
String name1 = ArrayAlg.getByIdx(names,"GameOver");
//會輸出 Unmi:Fantasia,或 Fantasia:Fantasia
System.out.println(name + ":" + name1);
}
}
因為現在還不想涉及到調用類型參數的特定方法,所以參照 HashMap 算法,由第二個類型參數算出數組范圍內的索引。留意兩種調用泛型方法的方式,應用隱式調用在有些情況下也是會產生二義性的。
本篇鏈接 http://unmi.cc/understand-java-generic-2
第三篇:
前面介紹的無論是泛型類還是泛型方法,基本上都是把定義的類型參數作為一個整體來操作,放到數組或取出來,頂多就是調用了一下 hashCode() 方法,因為這是 Java 的根對象擁有的方法。比如說一個泛型數組,要得到其中的最小元素:
package com.unmi;
/**
* 泛型,類型變量的限定
* @author Unmi
*/
public class ArrayAlg {
public static <T> T main(T[] a){
if(a==null || a.length ==0){
return null;
}
T smallest = a[0];
for(int i=0;i<a.length;i++){
if(smallest.compareTo(a[i])>0){
smallest = a[i];
}
}
return smallest;
}
}
在上面的代碼中,要比較大小,所以調用了類型 T 的 compareTo() 方法,我們期望具體類型是一個實現了 compareTo() 方法的類型。不過,很可惜,Java 泛型還是與 C++ 的模板有別,在 C++ 中真的對于類似上面的代碼,具體類型有 compareTo() 則可以通過,具體類型沒有 compareTo() 則不能通過,是推延至使用代碼時確定具體類型是否滿足條件。而在 Java 中無論你的具體類型是什么,上面的代碼本身就是無法編譯通過的,錯誤為:
The method compareTo(T) is undefined for the type T 也就是說在你編寫泛型的時候就該限類型參數的一個更窄的范圍,不是所有的 Object,而是那些實現了 compareTo() 方法的類型,比如說實現了 Compareable 接口的類型。為了做到這一點,對于前面方法聲明部分就要做如下進一步約束:
public static <T extends Comparable> T min(T[] a){......} 這里只表示到時的具體類型 T 是 Comparable 的一種類型,extends 后是接口,還是類都是用 extends 關鍵字,不在此區分接口還是類,只表示 Is-A 的關系。
這樣你在使用該泛型的時候具體類型一定要是實現了 Comparable 接口的類型,比如用這樣的代碼 ArrayAlg.main(new Object[]{"1","2","3"}); 調用,則會有下面的錯誤提示:
Bound mismatch: The generic method main(T[]) of type ArrayAlg is not applicable for the arguments (Object[]). The inferred type Object is not a valid substitute for the bounded parameter <T extends Comparable> ArrayAlg.java 因為 Object 并未實現 Comparable,用 ArrayAlg.main(new String[]{"1","2","3"}); 調用則無誤,String 是實現了 Comparable 接口的。
如果在泛型實現中會調用到多個方法,要求泛型參數同屬不同的類型,就 extends 多個接口或類,中間用 & 符號隔開。
public static <T extends Comparable & Serializable> T foo(T[] a){ //a[i].compareTo(a[i-1]); //xxx.writeObject(a[i]); }
為什么是用 & 而不用逗號(,) 呢,因為逗號還有作他用,當有多個類型參數時就該寫作:
public static <T extends Comparable & Serializable, U extends Runnable> T foo(T a, U b){ //a[i].compareTo(a[i-1]); //xxx.writeObject(a[i]); //new Thread(u).start(); }
雖然這里簡單的用 extends 統一的表示了原有的 extends 和 implements 的概念,但仍要遵循應用的體系,Java 只能繼承一個類,可以實現多個接口,所以你的某個類型需要用 extends 限定,有多種類型的時候,只能存在一個是類,而且參考變參的做法,類寫在第一位,接口列在后面,也就是:
<T extends SomeClass & interface1 & interface2 & interface3>
這里的例子僅演示了泛型方法的類型限定,對于泛型類中類型參數的限制用完全一樣的規則,只是加在類聲明的頭部,如:
public class Demo<T extends Comparable & Serializable>{
//該泛型類中就可以用 Comparable 聲明的方法和 Seriablizable 所擁有的特性了 }
多個類型參數的情況類推就是了。
本篇鏈接 http://unmi.cc/understand-java-generic-3
更多內容請關注原作者博客:隔葉黃鶯 Unmi Blog
總結
以上是生活随笔為你收集整理的步步理解 JAVA 泛型编程 – 共三篇的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 如何应用Java的可变参数
- 下一篇: java枚举使用详解