# 序言
一般的類和方法,只能使用具體的類型:要么是基本數據類型,要么是自定義的類。
如果要編寫可以應用于多種類型的代碼,這種刻板的限制對代碼的束縛就會很大。
?????????????????????????????????????????????????????????????????????????????????????????????????? —— 《Think in Java》
# 泛型程序設計帶來的好處是什么
不難想象,“可以應用于多種類型的代碼”這種需求當然是最初就存在的,因為代碼的復用一直是編碼的一個重點。
然而值得注意的是:泛型技術是是在Java 1.5版本之后才出現的。那么在此之前,對于上述的需求,Java是怎么樣來滿足的呢?
其實答案不難想象,當然是通過繼承的多態特性來實現的:超類類型聲明的對象引用能夠指向任何其子類對象。
而同時被眾所周知的還有:Java當中定義的任何類,都有一個默認的共同的超類:Object類。
那么自然的,如果想要一段代碼能夠接收任一類型的數據,那么該數據的類型自然就應當被定義為“Object”類型。
Java集合框架當中的容器類就基于泛型技術而實現,所以它們能夠保證你所定義的容器對象能夠接受任何指定類型的數據進行存儲。
而你已經知道集合框架于JDK 1.2之后就誕生了。那么在Java 1.2 - 1.5版本期間,它們是如何保證“能夠存儲任一對象類型”這一特征的呢?
答案并不讓人驚訝,當然同樣是基于繼承來實現的。正如當時的ArrayList容器類,內部只是維護一個Object類型引用的可變數組。
由此已經不難想象:正如Java中,原始數組已經可以完成對同一數據類型的多個數據進行存儲的工作。而在之后的升級中,卻衍生了集合框架的道理一樣。
既然通過繼承的多態特性,能夠完成同一段代碼應用于多種類型的目的。Java還是在升級中制定出了泛型技術,那么自然是因為原本的設計方式存在缺陷。
我們可以這樣考慮:假設想要編寫一個方法,方法能夠接受操作任一對象類型的參數。那么,在Java 1.5之前,其代碼自然是這樣的:
~~~
public class Demo {
void anyObject(Object o){
//some code..
}
}
~~~
我們不難看出使用這樣的方式,可能會造成的困擾。
首先,因為Java中的“動態綁定機制”。所以通過這樣的方式傳遞參數,在方法中只能調用到Object類自身的方法。
所以如果你想在方法內,調用你傳入的特定對象類型其自身額外的方法時,就必須涉及到強制類型轉換 - 完成“向下轉型”。
從而導致了蝴蝶效應:因為一旦涉及到類型的強制轉換,就可能會導致類型轉換異常:"ClassCastException"。
而泛型程序設計技術的出現,正是為了彌補上述實現方式的缺陷。所以說泛型的最大好處正是:
- 減少了代碼的復雜性:避免了在代碼中使用強制類型轉換的麻煩。
- 增強了代碼的安全性:將運行時異常“ClassCastException”轉到了編譯時檢測異常。
# 泛型的實際使用
泛型的使用規范,并不復雜,可以簡單歸納為:
- Java中用符號"<>"用以聲明使用泛型。括號內用以包含一個或多個泛型參數,多個參數之間用逗號隔開。
- 通常推薦使用簡練的名字來作為泛型參數的命名。最好避免小寫字母,這能很好的用以其和其他普通形式參數的區分。
- 如果一個泛型類里還包含有泛型方法,那么最好避免對方法的泛型類型參數與類的泛型參數使用同樣的標示符,避免混淆。
### 泛型類的定義
一個泛型類就是具有一個或多個類型變量的類。其定義格式通常為:class ClassName <T>。舉例來說:
~~~
package com.tsr.j2seoverstudy.generic;
public class GenericClassDemo<T> {
private T t;
GenericClassDemo(T t) {
this.t = t;
}
public T getT() {
return t;
}
public void setT(T t) {
this.t = t;
}
/*
* 輸出結果為:
* generic_class
* new value
*/
public static void main(String[] args) {
String genericVar = new String("generic_class");
GenericClassDemo<String> g = new GenericClassDemo<String>(genericVar);
System.out.println(g.getT());
g.setT("new value");
System.out.println(g.getT());
}
}
~~~
對于泛型類的使用,可以看到其特點就是:
在泛型類的內部,可以使用類聲明當中的類型參數,作為該類的成員變量的數據類型。
正如我們在上面的代碼中所定義的:“private T t;”。
而該類型參數,可以在該類進行構造工作時,被具體指定為具體的任意對象數據類型。
上面的例子中,我們為其制定的具體類型,是字符串類型“String”。這個過程就是“泛型的實例化”。
同時,泛型類當中定義的方法,可以接受和操作泛型類上聲明的類型參數。
但同時有一點特別需要注意的是:泛型的實例化工作是在對泛型類進行構造,生成對象的時候完成的。
這也解釋了為什么類中的靜態方法如果要使用泛型,泛型就必須被定義在該方法上,而不允許被定義在類上的原因。
正是因為一個類上聲明的泛型,本身就是依賴于該類的對象來明確的,但靜態方法卻不依賴于對象。
于是,接下來就讓我們來看一看泛型方法的定義和使用。
### 泛型方法的定義
泛型方法的定義格式為:public <T> 返回類型 方法名(T t)
關于泛型方法的定義,需要注意的就是:方法上的泛型應該放在方法修飾符和方法返回類型之間。
~~~
package com.tsr.j2seoverstudy.generic;
public class GenericMethodDemo {
/*
* 輸出結果為:
* 位于數組中間的數是:5
*/
public static void main(String[] args) {
Integer[] nums = { 1, 3, 5, 7, 9 };
System.out.println("位于數組中間的數是:" + getMiddle(nums));
}
static <T> T getMiddle(T[] t) {
return t[t.length / 2];
}
}
~~~
Java中的泛型載體有:泛型方法,泛型類,泛型接口。也就是說泛型除類與方法之外,還可以應用在接口上。
不過對于泛型接口的使用,在了解了泛型在類和方法的使用方式之后,你也應該差不多了解了。
### 泛型變量范圍的限定
有的時候,根據需求需要對類型變量加以約束。這時就涉及到了泛型變量的范圍限定的使用。
我們都知道實現compareable下的compareTo方法,可以用于對兩個對象的比較和排序。
那么假設我們定義了如下的泛型方法,想要舉出一個數組中compareTo方法比較下最大的對象結果:
~~~
package com.tsr.j2seoverstudy.generic;
public class GenericMethodDemo {
/*
* 輸出結果為:
* 最大的字符串元素是:zsda
*/
public static void main(String[] args) {
String [] strs = {"asdas","xzvzh","zsda","qwe"};
System.out.println("最大的字符串元素是:"+getMaxElement(strs));
}
static <T extends Comparable<T>> T getMaxElement(T[] t) {
if (t == null || t.length == 0)
return null;
T max = t[0];
for (int i = 1; i < t.length; i++) {
if (t[i].compareTo(max) > 0) {
max = t[i];
}
}
return max;
}
}
~~~
我們知道:最基本的泛型參數,可以用于接受任一對象數據類型。
但在上面的例子中,我們希望通過compareTo方法用于對象的比較。
什么樣的對象才具備compareTo方法,就是實現了Comparable接口的類的對象。
所以這個時候我們還必須對聲明的泛型做一個約束:指定的數據類型必須實現了Comparable接口。
這就是所謂的:泛型參數的范圍限定。
我們在上面的代碼中:“<T extends Comparable<T>>”就是一種用于限定泛型范圍的使用方式。
你可能注意到在這里我們使用了Java中原本用于表明類的繼承關系的關鍵字extends,用于指定泛型必須實現Comparable接口。
沒錯,在泛型當中,用于表示聲明的類型參數繼承自一個類或者實現自一個接口,都用extends表示。
這種限定的方式也常常被稱為:泛型的上限。其表現形式也就是:<泛型類型參數 extends 上限>。
注:如果聲明的泛型類型實現了多個接口,則其上限中的多個接口以符號”&“隔開。
?? 如果聲明的類型參數繼承自某個類,并同時實現了多個接口,那么其繼承的類必須被聲明在上限中的第一個。
既然有泛型的上限,就不難想象肯定存在與之對應的:泛型的下限。其限定方式為:<泛型類型參數 super? 下限>。
下限的限定代表,該泛型參數除開至少可以被聲明的下限類型實例化之外,還可以被該下限類型的所有超類類型所實例化。
舉例來說,有自定義的三個類:動物類、老虎類、東北虎類。它們之間的繼承關系是:東北虎繼承自老虎。老虎繼承自動物。
如果使用"<T super 老虎>"代表的含義就是:泛型參數T即可以被實例化為老虎,還可以被實例化為動物,但不能被實例化為東北虎。
# 泛型擦除
所謂的泛型擦除是指:當程序度過編譯器,進入到運行期后,在JVM中會將泛型去掉。這個過程就被稱為泛型的擦除。
之所以要進行泛型的擦除工作,實際上是因為:泛型實際上僅僅只是被Java編譯器所支持的的一項技術。而虛擬機是并不認識泛型的。
所以當一個Java程序使用到泛型技術的時候,首先會在編譯期經過編譯器的檢查:確定是否存在類型轉換的問題。
而當編譯通過,程序轉向運行期之后。在負責Java程序運行的虛擬機中,則會將泛型類型擦除。
所謂的泛型擦除就是指:將定義的泛型類型還原為其對應的原始類型(raw type)。這個還原的過程是根據泛型的限定類型確定的:
- 如果沒有聲明限定類型的類型參數則將被還原為:Object。
以上面我們說泛型類的定義時,所用到的例子來說,其還原形式就如同:
~~~
//泛型擦除前
public class GenericClassDemo<T> {
private T t;
GenericClassDemo(T t) {
this.t = t;
}
}
//泛型擦除后
public class GenericClassDemo {
private Object o;
GenericClassDemo(Object o) {
this.o = o;
}
}
~~~
- 如果泛型本身存在限定,則原始類型用限定的第一個類型變量代替。
例如說:
~~~
//泛型擦除之前
public class GerericDemo <T extends Comparable<T>&Serializable>{
private T t;
GerericDemo(T t){
this.t = t;
}
}
//泛型擦除之后
public class GerericDemo implements Serializable{
private Comparable t;
GerericDemo(Comparable t){
this.t = t;
}
}
~~~
所以實際上可以看到:所謂的泛型技術在使用時,最終還是會涉及到類型的強制轉換。但不同的是:使用泛型后的強制轉換是安全的!
之所以這么說,是因為我們在編譯時期已經確保了數據類型的一致性。如果你使用了指定泛型類型之外的數據類型,程序是不能編譯通過的。
那么既然話至于此,也正好通過一個例子,來看一看我們上面所談到的:“泛型將運行時異常“ClassCastException”轉到了編譯時期到底是指什么?
以容器類ArrayList為例,如果不通過泛型技術,那么假設使用以下這樣的方式:
~~~
package com.tsr.j2seoverstudy.generic;
import java.util.ArrayList;
public class GerericDemo{
public static void main(String[] args) {
ArrayList list = new ArrayList();
list.add(new String());
list.add(new Integer(5));
for (int i = 0; i < list.size(); i++) {
String value = (String)list.get(0);
}
}
}
~~~
上面的代碼編譯不會出現任何問題,但一旦運行就會報告運行時異常:java.lang.Integer cannot be cast to java.lang.String
這正是因為,如果沒有泛型的限定。ArrayList自身接受任何繼承自Object類的對象類型數據。
所以因為繼承的特性,我們要將定義的兩個分別為String與Integer類型的對象存放進該list容器是沒有問題的,因為它們自身都會完成一次“向上轉型”。
但也正是因為當完成“向上轉型”的工作后,實際上存放進list容器之后,它們的類型已經是Object類型了。
所以當我們再想要以特定類型將其取出時,就必須進行“向下轉型”,也就是強制類型轉換工作。
但Integer類型自身是不能夠強制轉換為Strring類型的,從而也就導致了類型轉換異常的出現。
反之,當容器類加入泛型技術之后,代碼則變為了:
~~~
public class GerericDemo{
public static void main(String[] args) {
ArrayList<String> list = new ArrayList<String>();
list.add(new String());
list.add(new Integer(5));//因為泛型的出現,編譯出錯!
for (int i = 0; i < list.size(); i++) {
//不再需要強轉
String value = list.get(i);
}
}
}
~~~
因為泛型類在構造時,必須為類型參數指定具體的類型,也就是完成泛型參數的實例化工作。
所以當我們將類型指定為<String>之后,再想要向容器內加入Integer類型的對象就會編譯出錯。
而同時因為編譯器已經知道該容器內存放的數據類型時String,所以在取出數據時,也就不再需要進行強轉了。
# 泛型通配符
Java當中的泛型通配符的符號是“?”。顧名思義,也就是指任何對象數據類型都可以通通匹配。
其主要的使用方式我們可以通過一個例子來看一下,假設我們定義了如下幾個類:
~~~
package com.tsr.j2seoverstudy.generic;
public class Animal<T> {
private T t;
Animal(T t) {
this.t = t;
}
public T getT() {
return t;
}
}
class Tiger {
@Override
public String toString() {
return "老虎";
}
}
class Bird {
@Override
public String toString() {
return "鳥";
}
}
~~~
假設我們想要定義一個工具類,用于輸出動物信息,如果沒有通配符的時候,實際上用起來是很不爽的:
~~~
package com.tsr.j2seoverstudy.generic;
public class GerericDemo {
public static void main(String[] args) {
Animal<Tiger> tiger = new Animal<Tiger>(new Tiger());
Animal<Bird> bird = new Animal<Bird>(new Bird());
printTiger(tiger);
printBird(bird);
}
static void printTiger(Animal<Tiger> animal){
System.out.println(animal.getT());
}
static void printBird(Animal<Bird> animal){
System.out.println(animal.getT());
}
}
~~~
因為我們已經說過了,泛型類在構造時必須指定類型參數的具體類型。所以你只能通過這種笨重的方式來實現需求。
但通過通配符,編碼的工作就變得輕松多了:
~~~
public class GerericDemo {
public static void main(String[] args) {
Animal<Tiger> tiger = new Animal<Tiger>(new Tiger());
Animal<Bird> bird = new Animal<Bird>(new Bird());
printAnimal(tiger);
printAnimal(bird);
}
static void printAnimal(Animal<?> animal){
System.out.println(animal.getT());
}
}
~~~
另外,泛型的通配符同樣可以用于泛型上限,下限的限定。
最后,順帶一提吧。注意下面一種錯誤的使用方式:
~~~
static void printAnimal(Animal<Tiger> animal){
System.out.println(animal.getT());
}
static void printAnimal(Animal<Bird> animal){
System.out.println(animal.getT());
}
~~~
一定要知道這樣的書寫方式是會導致編譯失敗的,而不要認為這是通過參數類型的不同對方法實現了重載。
泛型擦除!!!一定不要忘了這個工作。上面的代碼經泛型擦除之后,實際就變成了兩個一模一樣的方法聲明,自然是不能編譯通過的。
# 小結
到此,關于Java中泛型程序設計的應用,基本上已經是做了一個比較全面和詳細的總結了。
如果要繼續深入,可能就要自己再通過一些書籍和資料去研究泛型的原理以及java虛擬機方面的知識了。
個人感覺關于泛型擦除方面的底層原理還是十分復雜的,要深入掌握還是需要花一定精力的。
- 前言
- 第一個專欄《重走J2SE之路》,你是否和我有一樣的困擾?
- 磨刀不誤砍材工 - 環境搭建(為什么要配置環境變量)
- 磨刀不誤砍材工 - Java的基礎語言要素(定義良好的標示符)
- 磨刀不誤砍材工 - Java的基礎語言要素(關鍵字)
- 磨刀不誤砍材工 - Java的基礎語言要素(注釋-生成你自己的API說明文檔)
- 磨刀不誤砍材工 - Java的基礎語言要素(從變量/常量切入,看8種基本數據類型)
- 磨刀不誤砍材工 - Java的基礎語言要素(運算符和表達式的應用)
- 磨刀不誤砍材工 - Java的基礎語言要素(語句-深入理解)
- 磨刀不誤砍材工 - Java的基礎語言要素(數組)
- 換一個視角看事務 - 用&quot;Java語言&quot;寫&quot;作文&quot;
- 牛刀小試 - 淺析Java的繼承與動態綁定
- 牛刀小試 - 詳解Java中的接口與內部類的使用
- 牛刀小試 - 趣談Java中的異常處理
- 牛刀小試 - 詳解Java多線程
- 牛刀小試 - 淺析Java集合框架的使用
- 牛刀小試 - Java泛型程序設計
- 牛刀小試 - 詳細總結Java-IO流的使用