之前在寫公司項目的底層框架的時候用到一些泛型,實踐中涉及到一些沒關注到的細節,為此專門去Oracle官網把泛型的文檔學習了一遍。
Java中的泛型跟C++裡面的Template(模板)是同一個類型的東西,都是為了在其他地方調用的時候可以傳入各種參數類型。
在實踐中,與使用泛型有相似效果的是函數重載,即根據傳入參數類型的不同,選擇調用不同的函數。泛型和函數重載各有利弊,需要根據使用情景來選擇。如果一段代碼對於不同類型的參數,可以不做類型區分地使用,比如List的add方法,這時就用泛型。而如果一段代碼對於傳入的參數,應該根據不同的數據類型,執行不同的語句,這時就應該使用重載。為什麼?因為這時如果使用泛型,就會出現大量的instanceof判斷,判斷之後還有各種影響代碼質量的泛型與實際類型之間的類型強轉,而如果返回值也是泛型,那就更麻煩了。典型的就是之前對SharedPreferences進行封裝,對於不同類型的參數執行統一的get/put方法,但是如果傳入String類型,底層就要執行getString/putString方法,如果傳入int,就要執行getInt/putInt方法,這樣就必須使用如下的函數重載形式:
public static String get(String key, String defaultValue) { SharedPreferences sp = obtainPref(); return sp.getString(key, defaultValue); } public static int get(String key, int defaultValue) { SharedPreferences sp = obtainPref(); return sp.getInt(key, defaultValue); } public static boolean get(String key, boolean defaultValue) { SharedPreferences sp = obtainPref(); return sp.getBoolean(key, defaultValue); }
另外像JDK源碼裡面,StringBuilder的append方法,也是根據參數類型寫了一大堆看似啰嗦的重載函數,為什麼?因為方法體不一樣啊。
回歸正題,如果針對不同的參數類型,可以用同一段代碼,還是推薦用泛型的,畢竟可以把幾段代碼合並成一段代碼。
public class MyClass<T> { public static void main(String[] args) { MyClass<Integer> myClass = new MyClass<>(); myClass.printT(100); MyClass<Boolean> myClass2 = new MyClass<>(); myClass2.printT(true); } public void printT(T t) { System.out.print(t); }
}
請注意,這裡的泛型"T",代表的只能是Object類型,不能是int,boolean,char這些基本數據類型,比如像下面這樣寫就是錯的:
MyClass<int> myClass = new MyClass<>(); //報錯 myClass.printT(1);
也就是說,其實T是繼承自Object的。
那麼,為什麼定義的時候泛型參數必須是Object,而實際傳值的時候可以是100,true這些呢?因為JDK在編譯時做了一個自動裝箱的處理,把int類型包裝成了Integer類型,boolean類型則包裝成Boolean類型。可以參考我的另一篇blog: Java暗箱操作之自動裝箱與拆箱
代碼裡面每一個用到泛型參數 T, K, E,...都必須遵循先聲明再使用的原則,即如果你提到了這些泛型名稱,就必須在之前的某個地方被聲明過,否則會報錯。
泛型的聲明位置只能是兩個地方,一是類名處,二是方法處,別的地方都不能聲明。第一種方式,就是上面的 public class MyClass<T> {..}這種,在類名之後加,這樣在類裡面所有地方都能用"T"這個泛型參數。第二種方式在方法處聲明可能不太常見,之前我也不太熟悉,但項目裡確實用到了,只好研究一下,聲明格式類似於這樣:
public <T> T printT(T t) { System.out.print(t); }
這裡的T就只能作用於方法體了,而且會覆蓋類上聲明的泛型,例如以下代碼會正常運行:
public class MyClass<T> { public static void main(String[] args) { MyClass<String> myClass = new MyClass<>(); myClass.printT(100); } public <T> T printT(T t) { System.out.print(t); return t; } }
調用時,類上的泛型是String,方法上傳入的是Integer,那就以方法上的為准咯~
特別注意,方法上的泛型參數必須聲明在返回值之前,public/private之後,是有固定位置的。
可以對調用時傳入的泛型加限制條件,限制T必須是某個類(接口)的子類
public class MyClass<T extends Number> {...}
這裡,T就只能是Number或者Number的子類Integer,Float,Long這些,傳入String就是錯誤的。
T也可以繼承自多個類,注意這裡的類是泛指,包括接口在內,即寫成
public class MyClass<T extends A & B & C> {...}
其中A可以是類或接口,B、C只能是接口,即多繼承的話至多只能有一個是類,且必須把類寫在第一個。
傳入的泛型參數還可以是wildcard(通配符)
MyClass<?> myClass = new MyClass<>();
"?"是在調用時傳入的東西,取代String, Integer這些實際的類型。
有兩種情況會傳入"?":1、調用過程中僅涉及到Object的方法,像equals()等等;2、調用過程中不依賴於泛型。最典型的是Class<?>,因為調用的Class方法基本用不到泛型。
更多內容參考Oracle官方文檔:Generics