參考
為什么使用泛型
JAVA泛型總結(jié)
Java泛型:泛型類、泛型接口和泛型方法
java泛型應(yīng)用實(shí)例 - 自定義泛型類,方法
泛型的本質(zhì):數(shù)據(jù)類型參數(shù)化
為什么使用泛型
我們先看下面的代碼,代碼省略了一些內(nèi)容,但功能是實(shí)現(xiàn)一個棧,這個棧只能處理int數(shù)據(jù)類型:
public class Stack{
private int[] m_item;
public int Pop(){...}
public void Push(int item){...}
public Stack(int i)
{
this.m_item = new int[i];
}
}
上面代碼運(yùn)行的很好,但是,當(dāng)我們需要一個棧來保存string類型時,該怎么辦呢?很多人都會想到把上面的代碼復(fù)制一份,把int改成string不就行了。當(dāng)然,這樣做本身是沒有任何問題的,但一個優(yōu)秀的程序是不會這樣做的,因?yàn)樗氲饺粢院笤傩枰猯ong、Node類型的棧該怎樣做呢?還要再復(fù)制嗎?優(yōu)秀的程序員會想到用一個通用的數(shù)據(jù)類型object來實(shí)現(xiàn)這個棧:
public class Stack{
private object[] m_item;
public object Pop(){...}
public void Push(object item){...}
public Stack(int i)
{
this.m_item = new[i];
}
}
這個棧寫的不錯,他非常靈活,可以接收任何數(shù)據(jù)類型,可以說是一勞永逸。但全面地講,也不是沒有缺陷的,主要表現(xiàn)在:
當(dāng)Stack處理值類型時,會出現(xiàn)裝箱、折箱操作,這將在托管堆上分配和回收大量的變量,若數(shù)據(jù)量大,則性能損失非常嚴(yán)重。
在處理引用類型時,雖然沒有裝箱和折箱操作,但將用到數(shù)據(jù)類型的強(qiáng)制轉(zhuǎn)換操作,增加處理器的負(fù)擔(dān)。
在數(shù)據(jù)類型的強(qiáng)制轉(zhuǎn)換上還有更嚴(yán)重的問題(假設(shè)stack是Stack的一個實(shí)例):
Node1 x = new Node1();
stack.Push(x);
Node2 y = (Node2)stack.Pop();
上面的代碼在編譯時是完全沒問題的,但由于Push了一個Node1類型的數(shù)據(jù),但在Pop時卻要求轉(zhuǎn)換為Node2類型,這將出現(xiàn)程序運(yùn)行時的類型轉(zhuǎn)換異常,但卻逃離了編譯器的檢查。
泛型用用一個通過的數(shù)據(jù)類型T來代替object,在類實(shí)例化時指定T的類型,運(yùn)行時(Runtime)自動編譯為本地代碼,運(yùn)行效率和代碼質(zhì)量都有很大提高,并且保證數(shù)據(jù)類型安全。
使用泛型
下面是用泛型來重寫上面的棧,用一個通用的數(shù)據(jù)類型T來作為一個占位符,等待在實(shí)例化時用一個實(shí)際的類型來代替。讓我們來看看泛型的威力:
public class Stack<T>
{
private T[] m_item;
public T Pop(){...}
public void Push(T item){...}
public Stack(int i)
{
this.m_item = new T[i];
}
}
類的寫法不變,只是引入了通用數(shù)據(jù)類型T就可以適用于任何數(shù)據(jù)類型,并且類型安全的。這個類的調(diào)用方法:
//實(shí)例化只能保存int類型的類
Stack<int> a = new Stack<int>(100);
a.Push(10);
a.Push("8888"); //這一行編譯不通過,因?yàn)轭恆只接收int類型的數(shù)據(jù)
int x = a.Pop();
//實(shí)例化只能保存string類型的類
Stack<string> b = new Stack<string>(100);
b.Push(10); //這一行編譯不通過,因?yàn)轭恇只接收string類型的數(shù)據(jù)
b.Push("8888");
string y = b.Pop();
這個類和object實(shí)現(xiàn)的類有截然不同的區(qū)別:
- 他是類型安全的。實(shí)例化了int類型的棧,就不能處理string類型的數(shù)據(jù),其他數(shù)據(jù)類型也一樣。
- 無需裝箱和折箱。這個類在實(shí)例化時,按照所傳入的數(shù)據(jù)類型生成本地代碼,本地代碼數(shù)據(jù)類型已確定,所以無需裝箱和折箱。
- 無需類型轉(zhuǎn)換。
泛型類型
C#泛型類在編譯時,先生成中間代碼IL,通用類型T只是一個占位符。在實(shí)例化類時,根據(jù)用戶指定的數(shù)據(jù)類型代替T并由即時編譯器(JIT)生成本地代碼,這個本地代碼中已經(jīng)使用了實(shí)際的數(shù)據(jù)類型,等同于用實(shí)際類型寫的類,所以不同的封閉類的本地代碼是不一樣的。按照這個原理,我們可以這樣認(rèn)為:
泛型類的不同的封閉類是分別不同的數(shù)據(jù)類型。
例:Stack<int>和Stack<string>是兩個完全沒有任何關(guān)系的類,你可以把他看成類A和類B,這個解釋對泛型類的靜態(tài)成員的理解有很大幫助。
在JAVA中,對于不同傳入的類型實(shí)參,生成的相應(yīng)對象實(shí)例的類型是不是一樣的呢?
public class GenericTest {
public static void main(String[] args) {
Box<String> name = new Box<String>("corn");
Box<Integer> age = new Box<Integer>(712);
System.out.println("name class:" + name.getClass());
System.out.println("age class:" + age.getClass());
System.out.println(name.getClass() == age.getClass());// true
}
}
由此,我們發(fā)現(xiàn),在使用泛型類時,雖然傳入了不同的泛型實(shí)參,但并沒有真正意義上生成不同的類型,傳入不同泛型實(shí)參的泛型類在內(nèi)存上只有一個,即還是原來的最基本的類型(本實(shí)例中為Box),當(dāng)然,在邏輯上我們可以理解成多個不同的泛型類型。
究其原因,在于Java中的泛型這一概念提出的目的,導(dǎo)致其只是作用于代碼編譯階段,在編譯過程中,對于正確檢驗(yàn)泛型結(jié)果后,會將泛型的相關(guān)信息擦出,也就是說,成功編譯過后的class文件中是不包含任何泛型信息的。泛型信息不會進(jìn)入到運(yùn)行時階段。
對此總結(jié)成一句話:泛型類型在邏輯上看以看成是多個不同的類型,實(shí)際上都是相同的基本類型。
類型通配符
我們需要一個在邏輯上可以用來表示同時是Box<Integer>和Box<Number>的父類的一個引用類型,由此,類型通配符應(yīng)運(yùn)而生。
類型通配符一般是使用 ? 代替具體的類型實(shí)參。注意了,此處是類型實(shí)參,而不是類型形參!且Box<?>在邏輯上是Box<Integer>、Box<Number>...等所有Box<具體類型實(shí)參>的父類。由此,我們依然可以定義泛型方法,來完成此類需求。
public class GenericTest {
public static void main(String[] args) {
Box<String> name = new Box<String>("corn");
Box<Integer> age = new Box<Integer>(712);
Box<Number> number = new Box<Number>(314);
getData(name);
getData(age);
getData(number);
}
public static void getData(Box<?> data) {
System.out.println("data :" + data.getData());
}
}
有時候,我們還可能聽到類型通配符上限和類型通配符下限。具體有是怎么樣的呢?
在上面的例子中,如果需要定義一個功能類似于getData()的方法,但對類型實(shí)參又有進(jìn)一步的限制:只能是Number類及其子類。此時,需要用到類型通配符上限。
public class GenericTest {
public static void main(String[] args) {
Box<String> name = new Box<String>("corn");
Box<Integer> age = new Box<Integer>(712);
Box<Number> number = new Box<Number>(314);
getData(name);
getData(age);
getData(number);
//getUpperNumberData(name);//1
getUpperNumberData(age);//2
getUpperNumberData(number);//3
}
public static void getData(Box<?> data) {
System.out.println("data :" + data.getData());
}
public static void getUpperNumberData(Box<? extends Number> data){
System.out.println("data :" + data.getData());
}
}
此時,顯然,在代碼//1處調(diào)用將出現(xiàn)錯誤提示,而//2 //3處調(diào)用正常。
類型通配符上限通過形如Box<? extends Number>形式定義,相對應(yīng)的,類型通配符下限為Box<? super Number>形式,其含義與類型通配符上限正好相反