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