1. 程式人生 > >夯實JAVA基本之一 —— 泛型詳解(1):基本使用

夯實JAVA基本之一 —— 泛型詳解(1):基本使用

前言:無論何時,相信自己。

相關文章:

一、引入

1、泛型是什麼

首先告訴大家ArrayList就是泛型。那ArrayList能完成哪些想不到的功能呢?先看看下面這段程式碼:
ArrayList<String> strList = new ArrayList<String>();
ArrayList<Integer> intList = new ArrayList<Integer>();
ArrayList<Double> doubleList = new ArrayList<Double>();
大家對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<T>{// 此處可以隨便寫識別符號號 
    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<Integer> p = new Point<Integer>() ; 
p.setX(new Integer(100)) ; 
System.out.println(p.getX());  

//FloatPoint使用
Point<Float> p = new Point<Float>() ; 
p.setX(new Float(100.12f)) ; 
System.out.println(p.getX());  
先看看執行結果:

從結果中可以看到,我們實現了開篇中IntegerPoint類和FloatPoint類的效果。下面來看看泛型是怎麼定義及使用的吧。

(1)、定義泛型:Point<T>
首先,大家可以看到Point<T>,即在類名後面加一個尖括號,括號裡是一個大寫字母。這裡寫的是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<Integer> p = new Point<Integer>() ; 
p.setX(new Integer(100)) ; 
System.out.println(p.getX());  

//FloatPoint使用
Point<Float> p = new Point<Float>() ; 
p.setX(new Float(100.12f)) ; 
System.out.println(p.getX());  
首先,是構造一個例項:
Point<String> p = new Point<String>() ; 
這裡與普通構造類例項的不同之點在於,普通類建構函式是這樣的:Point p = new Point() ;
而泛型類的構造則需要在類名後新增上<String>,即一對尖括號,中間寫上要傳入的型別。
因為我們構造時,是這樣的:class Point<T>,所以在使用的時候也要在Point後加上型別來定義T代表的意義。
然後在getVar()和setVar()時就沒有什麼特殊的了,直接呼叫即可。
從上面的使用時,明顯可以看出泛型的作用,在構造泛型類的例項的時候:
//IntegerPoint使用
Point<Integer> p = new Point<Integer>() ; 
//FloatPoint使用
Point<Float> p = new Point<Float>() ; 
尖括號中,你傳進去的是什麼,T就代表什麼型別。這就是泛型的最大作用,我們只需要考慮邏輯實現,就能拿給各種類來用。
前面我們提到ArrayList也是泛型,我們順便它的實現:
public class ArrayList<E>{
	…………
}
看到了吧,跟我們的Point實現是一樣的,這也就是為什麼ArrayList能夠盛裝各種型別的主要原因。
(4)使用泛型實現的優勢
相比我們開篇時使用Object的方式,有兩個優點:
(1)、不用強制轉換
//使用Object作為返回值,要強制轉換成指定型別
Float floatX = (Float)floatPoint.getX();
//使用泛型時,不用強制轉換,直接出來就是String
System.out.println(p.getVar()); 
(2)、在settVar()時如果傳入型別不對,編譯時會報錯

可以看到,當我們構造時使用的是String,而在setVar時,傳進去Integer型別時,就會報錯。而不是像Object實現方式一樣,在執行時才會報強制轉換錯誤。

2、多泛型變數定義及字母規範

(1)、多泛型變數定義
上在我們只定義了一個泛型變數T,那如果我們需要傳進去多個泛型要怎麼辦呢?
只需要在類似下面這樣就可以了:
class MorePoint<T,U>{
}
也就是在原來的T後面用逗號隔開,寫上其它的任意大寫字母即可。想加幾個就加幾個,比如我們想加五個泛型變數,那應該是這樣的:
class MorePoint<T,U,A,B,C>{
}
舉個粟子,我們在Point上再另加一個欄位name,也用泛型來表示,那要怎麼做?程式碼如下:
class MorePoint<T,U> {
    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<Integer,String> morePoint = new MorePoint<Integer, String>();
morePoint.setName("harvic");
Log.d(TAG, "morPont.getName:" + morePoint.getName());
從上面的程式碼中,可以明顯看出,就是在新新增的泛型變數U用法與T是一樣的。
(2)、字母規範
在定義泛型類時,我們已經提到用於指定泛型的變數是一個大寫字母:
class Point<T>{
 …………
}
當然不是的!!!!任意一個大寫字母都可以。他們的意義是完全相同的,但為了提高可讀性,大家還是用有意義的字母比較好,一般來講,在不同的情境下使用的字母意義如下:
  •  E — Element,常用在java Collection裡,如:List<E>,Iterator<E>,Set<E>
  •  K,V — Key,Value,代表Map的鍵值對
  •  N — Number,數字
  •  T — Type,型別,如String,Integer等等
如果這些還不夠用,那就自己隨便取吧,反正26個英文字母呢。
再重複一遍,使用哪個字母是沒有特定意義的!只是為了提高可讀性!!!!

3、泛型介面定義及使用

在介面上定義泛型與在類中定義泛型是一樣的,程式碼如下:

interface Info<T>{        // 在介面上定義泛型  
    public T getVar() ; // 定義抽象方法,抽象方法的返回值就是泛型型別  
    public void setVar(T x);
}  

與泛型類的定義一樣,也是在介面名後加尖括號;
(1)、使用方法一:非泛型類
但是在使用的時候,就出現問題了,我們先看看下面這個使用方法:

class InfoImpl implements Info<String>{	// 定義泛型介面的子類
    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<String>{	
 …………
}
要清楚的一點是InfoImpl不是一個泛型類!因為他類名後沒有<T>!
然後在在這裡我們將Info<String>中的泛型變數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<T>介面給填充好了,但我們的類,是可以構造成泛型類的,那我們利用泛型類來構造填充泛型介面會是怎樣呢?

interface Info<T>{		// 在介面上定義泛型
	public T getVar() ;	// 定義抽象方法,抽象方法的返回值就是泛型型別
	public void setVar(T var);
}
class InfoImpl<T> implements Info<T>{	// 定義泛型介面的子類
	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<String> i = new InfoImpl<String>("harvic");
		System.out.println(i.getVar()) ;
	}
};
最關鍵的是構造泛型類的過程:
class InfoImpl<T> implements Info<T>{	// 定義泛型介面的子類
	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>,然後把泛型變數T傳給了Info<T>,這說明介面和泛型類使用的都是同一個泛型變數。
然後在使用時,就是構造一個泛型類的例項的過程,使用過程也不變。
public class GenericsDemo24{
	public static void main(String arsg[]){
		Info<String> i = new InfoImpl<String>("harvic");
		System.out.println(i.getVar()) ;
	}
};
使用泛型類來繼承泛型介面的作用就是讓使用者來定義介面所使用的變數型別,而不是像方法一那樣,在類中寫死。
那我們稍微加深點難度,構造一個多個泛型變數的類,並繼承自Info介面:
class InfoImpl<T,K,U> implements Info<U>{	// 定義泛型介面的子類
     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<Integer,Double,String> i = new InfoImpl<Integer,Double,String>("harvic");
        System.out.println(i.getVar()) ;
    }
}

4、泛型函式定義及使用

上面我們講解了類和介面的泛型使用,下面我們再說說,怎麼單獨在一個函式裡使用泛型。比如我們在新建一個普通的類StaticFans,然後在其中定義了兩個泛型函式:
public class StaticFans {
	//靜態函式
    public static  <T> void StaticMethod(T a){
        Log.d("harvic","StaticMethod: "+a.toString());
    }
	//普通函式
    public  <T> void OtherMethod(T a){
        Log.d("harvic","OtherMethod: "+a.toString());
    }
}
上面分別是靜態泛型函式和常規泛型函式的定義方法,與以往方法的唯一不同點就是在返回值前加上<T>來表示泛型變數。其它沒什麼區別。
使用方法如下:
//靜態方法
StaticFans.StaticMethod("adfdsa");//使用方法一
StaticFans.<String>StaticMethod("adfdsa");//使用方法二

//常規方法
StaticFans staticFans = new StaticFans();
staticFans.OtherMethod(new Integer(123));//使用方法一
staticFans.<Integer>OtherMethod(new Integer(123));//使用方法二
結果如下:

首先,我們看靜態泛型函式的使用方法:

StaticFans.StaticMethod("adfdsa");//使用方法一
StaticFans.<String>StaticMethod("adfdsa");//使用方法二
從結果中我們可以看到,這兩種方法的結果是完全一樣的,但他們還有些區別的,區別如下:
方法一,可以像普通方法一樣,直接傳值,任何值都可以(但必須是派生自Object類的型別,比如String,Integer等),函式會在內部根據傳進去的引數來識別當前T的類別。但儘量不要使用這種隱式的傳遞方式,程式碼不利於閱讀和維護。因為從外觀根本看不出來你呼叫的是一個泛型函式。
方法二,與方法一不同的地方在於,在呼叫方法前加了一個<String>來指定傳給<T>的值,如果加了這個<String>來指定引數的值的話,那StaticMethod()函式裡所有用到的T型別也就是強制指定了是String型別。這是我們建議使用的方式。
同樣,常規泛型函式的使用也有這兩種方式:
StaticFans staticFans = new StaticFans();
staticFans.OtherMethod(new Integer(123));//使用方法一
staticFans.<Integer>OtherMethod(new Integer(123));//使用方法二
可以看到,與平常一樣,先建立類的例項,然後呼叫泛型函式。
方法一,隱式傳遞了T的型別,與上面一樣,不建議這麼做。
方法二,顯示將T賦值為Integer型別,這樣OtherMethod(T a)傳遞過來的引數如果不是Integer那麼編譯器就會報錯。

進階:返回值中存在泛型
上面我們的函式中,返回值都是void,但現實中不可能都是void,有時,我們需要將泛型變數返回,比如下面這個函式:
public static <T> List<T> parseArray(String response,Class<T> object){
    List<T> modelList = JSON.parseArray(response, object);
    return modelList;
}
函式返回值是List<T>型別。至於傳入引數Class<T> object的意義,我們下面會講。這裡也就是想通過這個例子來告訴大家,泛型變數其實跟String,Integer,Double等等的類的使用上沒有任何區別,T只是一個符號,可以代表String,Integer,Double……這些類的符號,在泛型函式使用時,直接把T看到String,Integer,Double……中的任一個來寫程式碼就可以了。唯一不同的是,要在函式定義的中在返回值前加上<T>標識泛型;

5、其它用法:Class<T>類傳遞及泛型陣列

(1)、使用Class<T>傳遞泛型類Class物件
有時,我們會遇到一個情況,比如,我們在使用JSON解析字串的時候,程式碼一般是這樣的
public static List<SuccessModel> parseArray(String response){
    List<SuccessModel> modelList = JSON.parseArray(response, SuccessModel.class);
    return modelList;
}
其中SuccessModel是自定義的解析類,程式碼如下,其實大家不用管SuccessModel的定義,只考慮上面的那段程式碼就行了。寫出來SuccessModel的程式碼,只是不想大家感到迷惑,其實,這裡只是fastJson的基本用法而已。
這段程式碼的意義就是根據SuccessModel解析出List<SuccessModel>的陣列。
public class SuccessModel {
    private boolean success;
    
    public boolean isSuccess() {
        return success;
    }

    public void setSuccess(boolean success) {
        this.success = success;
    }
} 
那現在,我們把下面這句組裝成一個泛型函式要怎麼來做呢?
public static List<SuccessModel> parseArray(String response){
    List<SuccessModel> modelList = JSON.parseArray(response, SuccessModel.class);
    return modelList;
}
首先,我們應該把SuccessModel單獨抽出來做為泛型變數,但parseArray()中用到的SuccessModel.class要怎麼弄呢?
先來看程式碼:
public static <T> List<T> parseArray(String response,Class<T> object){
    List<T> modelList = JSON.parseArray(response, object);
    return modelList;
}
注意到,我們用的Class<T> object來傳遞類的class物件,即我們上面提到的SuccessModel.class。
這是因為Class<T>也是一泛型,它是傳來用來裝載類的class物件的,它的定義如下:
public final class Class<T> implements Serializable {
	…………
}
通過Class<T>來載入泛型的Class物件的問題就講完了,下面來看看泛型陣列的使用方法吧。
(2)、定義泛型陣列
在寫程式時,大家可能會遇到類似String[] list = new String[8];的需求,這裡可以定義String陣列,當然我們也可以定義泛型陣列,泛型陣列的定義方法為 T[],與String[]是一致的,下面看看用法:
//定義
public static <T> 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> T[] fun1(T...arg){  // 接收可變引數  
       return arg ;            // 返回泛型陣列  
}  
首先,定義了一個靜態函式,然後定義返回值為T[],引數為接收的T型別的可變長引數。如果有同學對T...arg的用法不瞭解,可以去找下JAVA 可變長引數方面的知識。
由於可變長引數在輸入後,會儲存在arg這個陣列中,所以,我們直接把陣列返回即可。

好了,這篇到這裡就結束了,這篇中主要講述了泛型在各方面的定義及用法,下篇,我們將講述,有關泛型限定相關的知識。

如果本文有幫到你,記得加關注哦