夯實JAVA基本之一——泛型詳解(1)
一、引入
1、泛型是什麼首先告訴大家ArrayList就是泛型。那ArrayList能完成哪些想不到的功能呢?先看看下面這段代碼:
ArrayList strList = new ArrayList();ArrayList intList = new ArrayList();ArrayList doubleList = new ArrayList();
大家對ArrayList很熟悉,這裡構造了三個List,分別盛裝String、Integer和Double;這就是ArrayList的過人之處:即各種類型的變數都可以組裝成對應的List,而不必針對每個對應分別實現一個構建ArrayList的類。這裡可能看不懂,開篇總是困難的,下面看看如果沒有泛型的話,我們要怎麼做;
2、沒有泛型會怎樣先看下面這段代碼:
我們實現兩個能夠設定點座標的類,分別設定Integer類型的點座標和Float類型的點座標:
//設定Integer類型的點座標class IntegerPoint{ private Integer x ; // 表示X座標 private Integer y ; // 表示Y座標 public void setX(Integer x){ this.x = x ; } public void setY(Integer y){ this.y = y ; } public Integer getX(){ return this.x ; } public Integer getY(){ return this.y ; }}//設定Float類型的點座標class FloatPoint{ private Float x ; // 表示X座標 private Float y ; // 表示Y座標 public void setX(Float x){ this.x = x ; } public void setY(Float y){ this.y = y ; } public Float getX(){ return this.x ; } public Float getY(){ return this.y ; }}
那現在有個問題:大家有沒有發現,他們聊了變數類型不一樣,一個是Integer一個是Float以外,其它並沒有什麼區別!那我們能不能合并成一個呢?
答案是可以的,因為Integer和Float都是派生自Object的,我們用下面這段代碼代替:
class ObjectPoint{ private Object x ; private Object y ; public void setX(Object x){ this.x = x ; } public void setY(Object y){ this.y = y ; } public Object getX(){ return this.x ; } public Object getY(){ return this.y ; }}
即全部都用Object來代替所有的子類;
在取的時候是這樣的:
ObjectPoint integerPoint = new ObjectPoint();integerPoint.setX(new Integer(100));Integer integerX=(Integer)integerPoint.getX();
在設定的時候,使用new Integer(100)來建立一個Integer
integerPoint.setX(new Integer(100));
然後在取值的時候,進行強制轉換:
Integer integerX=(Integer)integerPoint.getX();
由於我們設定的時候,是設定的Integer,所以在取值的時候,強制轉換是不會出錯的。
同理,FloatPoint的設定和取值也是類似的,代碼如下:
ObjectPoint floatPoint = new ObjectPoint();floatPoint.setX(new Float(100.12f));Float floatX = (Float)floatPoint.getX();
但問題來了:注意,注意,我們這裡使用了強制轉換,我們這裡setX()和getX()寫得很近,所以我們明確的知道我們傳進去的是Float類型,那如果我們記錯了呢?
比如我們改成下面這樣,編譯時間會報錯嗎:
ObjectPoint floatPoint = new ObjectPoint();floatPoint.setX(new Float(100.12f));String floatX = (String)floatPoint.getX();
不會!!!我們問題的關鍵在於這句:
String floatX = (String)floatPoint.getX();
強制轉換時,會不會出錯。因為編譯器也不知道你傳進去的是什麼,而floatPoint.getX()返回的類型是Object,所以編譯時間,將Object強轉成String是成立的。必然不會報錯。
而在運行時,則不然,在運行時,floatPoint執行個體中明明傳進去的是Float類型的變數,非要把它強轉成String類型,肯定會報類型轉換錯誤的!
那有沒有一種辦法在編譯階段,即能合并成同一個,又能在編譯時間檢查出來傳進去類型不對呢?當然,這就是泛型。
下面我們將對泛型的寫法和用法做一一講解。
二、各種泛型定義及使用
1、泛型類定義及使用我們先看看泛型的類是怎麼定義的:
//定義class Point{// 此處可以隨便寫標識符號 private T x ; private T y ; public void setX(T x){//作為參數 this.x = x ; } public void setY(T y){ this.y = y ; } public T getX(){//作為傳回值 return this.x ; } public T getY(){ return this.y ; }};//IntegerPoint使用Point p = new Point() ; p.setX(new Integer(100)) ; System.out.println(p.getX()); //FloatPoint使用Point p = new Point() ; p.setX(new Float(100.12f)) ; System.out.println(p.getX());
先看看運行結果:
從結果中可以看到,我們實現了開篇中IntegerPoint類和FloatPoint類的效果。下面來看看泛型是怎麼定義及使用的吧。
(1)、定義泛型:Point
首先,大家可以看到Point,即在類名後面加一個角括弧,括弧裡是一個大寫字母。這裡寫的是T,其實這個字母可以是任何大家字母,大家這裡先記著,可以是任何大寫字母,意義是相同的。
(2)類中使用泛型
這個T表示派生天Object類的任何類,比如String,Integer,Double等等。這裡要注意的是,T一定是派生於Object類的。為方便起見,大家可以在這裡把T當成String,即String在類中怎麼用,那T在類中就可以怎麼用!所以下面的:定義變數,作為傳回值,作為參數傳入的定義就很容易理解了。
//定義變數private T x ; //作為傳回值public T getX(){ return x ; } //作為參數public void setX(T x){ this.x = x ; }
(3)使用泛型類
下面是泛型類的用法:
//IntegerPoint使用Point p = new Point() ; p.setX(new Integer(100)) ; System.out.println(p.getX()); //FloatPoint使用Point p = new Point() ; p.setX(new Float(100.12f)) ; System.out.println(p.getX());
首先,是構造一個執行個體:
Point p = new Point() ;
這裡與普通構造類執行個體的不同之點在於,普通類建構函式是這樣的:Point p = new Point() ;
而泛型類的構造則需要在類名後添加上,即一對角括弧,中間寫上要傳入的類型。
因為我們構造時,是這樣的:class Point,所以在使用的時候也要在Point後加上類型來定義T代表的意義。
然後在getVar()和setVar()時就沒有什麼特殊的了,直接調用即可。
從上面的使用時,明顯可以看出泛型的作用,在構造泛型類的執行個體的時候:
//IntegerPoint使用Point p = new Point() ; //FloatPoint使用Point p = new Point() ;
角括弧中,你傳進去的是什麼,T就代表什麼類型。這就是泛型的最大作用,我們只需要考慮邏輯實現,就能拿給各種類來用。
前面我們提到ArrayList也是泛型,我們順便它的實現:
public class ArrayList{…………}
看到了吧,跟我們的Point實現是一樣的,這也就是為什麼ArrayList能夠盛裝各種類型的主要原因。
(4)使用泛型實現的優勢
相比我們開篇時使用Object的方式,有兩個優點:
(1)、不用強制轉換
//使用Object作為傳回值,要強制轉換成指定類型Float floatX = (Float)floatPoint.getX();//使用泛型時,不用強制轉換,直接出來就是StringSystem.out.println(p.getVar());
(2)、在settVar()時如果傳入類型不對,編譯時間會報錯
可以看到,當我們構造時使用的是String,而在setVar時,傳進去Integer類型時,就會報錯。而不是像Object實現方式一樣,在運行時才會報強制轉換錯誤。
2、多泛型變數定義及字母規範
(1)、多泛型變數定義
上在我們只定義了一個泛型變數T,那如果我們需要傳進去多個泛型要怎麼辦呢?
只需要在類似下面這樣就可以了:
class MorePoint{}
也就是在原來的T後面用逗號隔開,寫上其它的任意大寫字母即可。想加幾個就加幾個,比如我們想加五個泛型變數,那應該是這樣的:
class MorePoint{}
舉個粟子,我們在Point上再另加一個欄位name,也用泛型來表示,那要怎麼做?代碼如下:
class MorePoint { private T x; private T y; private U name; public void setX(T x) { this.x = x; } public T getX() { return this.x; }………… public void setName(U name){ this.name = name; } public U getName() { return this.name; }}//使用MorePoint morePoint = new MorePoint();morePoint.setName(harvic);Log.d(TAG, morPont.getName: + morePoint.getName());
從上面的代碼中,可以明顯看出,就是在新添加的泛型變數U用法與T是一樣的。
(2)、字母規範
在定義泛型類時,我們已經提到用於指定泛型的變數是一個大寫字母:
class Point{ …………}
當然不是的!!!!任意一個大家字母都可以。他們的意義是完全相同的,但為了提高可讀性,大家還是用有意義的字母比較好,一般來講,在不同的情境下使用的字母意義如下:
E — Element,常用在java Collection裡,如:List,Iterator,Set K,V — Key,Value,代表Map的索引值對 N — Number,數字 T — Type,類型,如String,Integer等等如果這些還不夠用,那就自己隨便取吧,反正26個英文字母呢。
再重複一遍,使用哪個字母是沒有特定意義的!只是為了提高可讀性!!!!
3、泛型介面定義及使用在介面上定義泛型與在類中定義泛型是一樣的,代碼如下:
與泛型類的定義一樣,也是在類名後加角括弧;
(1)、使用方法一:非泛型類
但是在使用的時候,就出現問題了,我們先看看下面這個使用方法:
class InfoImpl implements Info{// 定義泛型介面的子類 private String var ;// 定義屬性 public InfoImpl(String var){// 通過構造方法設定屬性內容 this.setVar(var) ; } @Override public void setVar(String var){ this.var = var ; } @Override public String getVar(){ return this.var ; }}public class GenericsDemo24{ public void main(String arsg[]){ InfoImpl i = new InfoImpl(harvic); System.out.println(i.getVar()) ; }};
首先,先看InfoImpl的定義:
class InfoImpl implements Info{ …………}
要清楚的一點是InfoImpl不是一個泛型類!因為他類名後沒有!
然後在在這裡我們將Info中的泛型變數T定義填充為了String類型。所以在重寫時setVar()和getVar()時,IDE會也我們直接產生String類型的重寫函數。
最後在使用時,沒什麼難度,傳進去String類型的字串來構造InfoImpl執行個體,然後調用它的函數即可。
public class GenericsDemo24{ public void main(String arsg[]){ InfoImpl i = new InfoImpl(harvic); System.out.println(i.getVar()) ; }};
(2)、使用方法二:泛型類
在方法一中,我們在類中直接把Info介面給填充好了,但我們的類,是可以構造成泛型類的,那我們利用泛型類來構造填充泛型介面會是怎樣呢?
interface Info{// 在介面上定義泛型public T getVar() ;// 定義抽象方法,抽象方法的傳回值就是泛型型別}class InfoImpl implements Info{// 定義泛型介面的子類private T var ;// 定義屬性public InfoImpl(T var){// 通過構造方法設定屬性內容this.setVar(var) ;}public void setVar(T var){this.var = var ;}public T getVar(){return this.var ;}}public class GenericsDemo24{public static void main(String arsg[]){InfoImpl i = new InfoImpl(harvic);System.out.println(i.getVar()) ;}};
最關鍵的是構造泛型類的過程:
class InfoImpl implements Info{// 定義泛型介面的子類private T var ;// 定義屬性public InfoImpl(T var){// 通過構造方法設定屬性內容this.setVar(var) ;}public void setVar(T var){this.var = var ;}public T getVar(){return this.var ;}}
在這個類中,我們構造了一個泛型類InfoImpl,然後把泛型變數T傳給了Info,這說明介面和泛型類使用的都是同一個泛型變數。
然後在使用時,就是構造一個泛型類的執行個體的過程,使用過程也不變。
public class GenericsDemo24{public static void main(String arsg[]){Info i = new InfoImpl(harvic);System.out.println(i.getVar()) ;}};
使用泛型類來繼承泛型介面的作用就是讓使用者來定義介面所使用的變數類型,而不是像方法一那樣,在類中寫死。
那我們稍微加深點難度,構造一個多個泛型變數的類,並繼承自Info介面:
class InfoImpl implements Info{// 定義泛型介面的子類 private U var ; private T x; private K y; public InfoImpl(U var){// 通過構造方法設定屬性內容 this.setVar(var) ; } public void setVar(U var){ this.var = var ; } public U getVar(){ return this.var ; } }
在這個例子中,我們在泛型類中定義三個泛型變數T,K,U並且把第三個泛型變數U用來填充介面Info。所以在這個例子中Info所使用的類型就是由U來決定的。
使用時是這樣的:泛型類的基本用法,不再多講,代碼如下:
public class GenericsDemo24{ public void main(String arsg[]){ InfoImpl i = new InfoImpl(harvic); System.out.println(i.getVar()) ; }}
4、泛型函數定義及使用
上面我們講解了類和介面的泛型使用,下面我們再說說,怎麼單獨在一個函數裡使用泛型。比如我們在建立一個普通的類StaticFans,然後在其中定義了兩個泛型函數:
public class StaticFans {//靜態函數 public static void StaticMethod(T a){ Log.d(harvic,StaticMethod: +a.toString()); }//普通函數 public void OtherMethod(T a){ Log.d(harvic,OtherMethod: +a.toString()); }}
上面分別是靜態泛型函數和常規泛型函數的定義方法,與以往方法的唯一不同點就是在傳回值前加上來表示泛型變數。其它沒什麼區別。
使用方法如下:
//靜態方法StaticFans.StaticMethod(adfdsa);//使用方法一StaticFans.StaticMethod(adfdsa);//使用方法二//常規方法StaticFans staticFans = new StaticFans();staticFans.OtherMethod(new Integer(123));//使用方法一staticFans.OtherMethod(new Integer(123));//使用方法二
結果如下:
首先,我們看靜態泛型函數的使用方法:
StaticFans.StaticMethod(adfdsa);//使用方法一StaticFans.StaticMethod(adfdsa);//使用方法二
從結果中我們可以看到,這兩種方法的結果是完全一樣的,但他們還有些區別的,區別如下:
方法一,可以像普通方法一樣,直接傳值,任何值都可以(但必須是派生自Object類的類型,比如String,Integer等),函數會在內部根據傳進去的參數來識別當前T的類別。但盡量不要使用這種隱式的傳遞方式,代碼不利於閱讀和維護。因為從外觀根本看不出來你調用的是一個泛型函數。
方法二,與方法一不同的地方在於,在調用方法前加了一個來指定傳給的值,如果加了這個來指定參數的值的話,那StaticMethod()函數裡所有用到的T類型也就是強制指定了是String類型。這是我們建議使用的方式。
同樣,常規泛型函數的使用也有這兩種方式:
StaticFans staticFans = new StaticFans();staticFans.OtherMethod(new Integer(123));//使用方法一staticFans.OtherMethod(new Integer(123));//使用方法二
可以看到,與平常一樣,先建立類的執行個體,然後調用泛型函數。
方法一,隱式傳遞了T的類型,與上面一樣,不建議這麼做。
方法二,顯示將T賦值為Integer類型,這樣OtherMethod(T a)傳遞過來的參數如果不是Integer那麼編譯器就會報錯。
進階:傳回值中存在泛型上面我們的函數中,傳回值都是void,但現實中不可能都是void,有時,我們需要將泛型變數返回,比如下面這個函數:
public static List parseArray(String response,Class object){ List modelList = JSON.parseArray(response, object); return modelList;}
函數傳回值是List類型。至於傳入參數Class object的意義,我們下面會講。這裡也就是想通過這個例子來告訴大家,泛型變數其實跟String,Integer,Double等等的類的使用上沒有任何區別,T只是一個符號,可以代表String,Integer,Double……這些類的符號,在泛型函數使用時,直接把T看到String,Integer,Double……中的任一個來寫代碼就可以了。唯一不同的是,要在函數定義的中在傳回值前加上標識泛型;
5、其它用法:Class類傳遞及泛型數組
(1)、使用Class傳遞泛型類Class對象有時,我們會遇到一個情況,比如,我們在使用JSON解析字串的時候,代碼一般是這樣的
public static List parseArray(String response){ List modelList = JSON.parseArray(response, SuccessModel.class); return modelList;}
其中SuccessModel是自訂的解析類,代碼如下,其實大家不用管SuccessModel的定義,只考慮上面的那段代碼就行了。寫出來SuccessModel的代碼,只是不想大家感到迷惑,其實,這裡只是fastJson的基本用法而已。
這段代碼的意義就是根據SuccessModel解析出List的數組。
public class SuccessModel { private boolean success; public boolean isSuccess() { return success; } public void setSuccess(boolean success) { this.success = success; }}
那現在,我們把下面這句組裝成一個泛型函數要怎麼來做呢?
public static List parseArray(String response){ List modelList = JSON.parseArray(response, SuccessModel.class); return modelList;}
首先,我們應該把SuccessModel單獨抽出來做為泛型變數,但parseArray()中用到的SuccessModel.class要怎麼弄呢?
先來看代碼:
public static List parseArray(String response,Class object){ List modelList = JSON.parseArray(response, object); return modelList;}
注意到,我們用的Class object來傳遞類的class對象,即我們上面提到的SuccessModel.class。
這是因為Class也是一泛型,它是傳來用來裝載類的class對象的,它的定義如下:
public final class Class implements Serializable {…………}
通過Class來載入泛型的Class對象的問題就講完了,下面來看看泛型數組的使用方法吧。
(2)、定義泛型數組在寫程式時,大家可能會遇到類似String[] list = new String[8];的需求,這裡可以String數組,當然我們也可以定義泛型數組,泛型數組的定義方法為 T[],與String[]是一致的,下面看看用法:
//定義public static T[] fun1(T...arg){ // 接收可變參數 return arg ; // 返回泛型數組 } //使用public static void main(String args[]){ Integer i[] = fun1(1,2,3,4,5,6) ; Integer[] result = fun1(i) ;}
我們先看看 定義時的代碼:
public static T[] fun1(T...arg){ // 接收可變參數 return arg ; // 返回泛型數組 }
首先,定義了一個靜態函數,然後定義傳回值為T[],參數為接收的T類型的可變長參數。如果有同學對T...arg的用法不瞭解,可以去找下JAVA 可變長參數方面的知識。
由於可變長參數在輸入用,會儲存在arg這個數組中,所以,我們直接把數組返回即可。
好了,這篇到這裡就結束了,這篇中主要講述了泛型在各方面的定義及用法,下篇,我們將講述,有關泛型限定和編譯時間泛型擦除相關的知識。