泛型是 Java 的一個高階特性。在 Mybatis、Hibernate 這種持久化框架,泛型更是無處不在。
然而,泛型畢竟是高階特性,藏在框架的底層程式碼裡面。我們平時都是寫業務程式碼,可能從來沒見過泛型,更別提怎麼用了。
既然如此,我們就一步步學習泛型吧。
泛型是什麼泛型是一種特殊的型別。你不用一開始就指明引數的具體型別,而是先定義一個型別變數,在使用的時候再確定引數的具體型別。
這好像還是很難理解。沒關係,我們先來看看,在沒有泛型情況下,我們是怎麼做的。
你能寫一個通用方法,把這些敏感欄位設定為空嗎?
你可能想到了,在 Java 中,所有的類都繼承了 Object 。於是,你寫出了第一個版本。
public class ApplicationV1 { // 把敏感欄位設定為空 public static Object removeField(Object obj) throws Exception { // 需要過濾的敏感欄位 Set<String> fieldSet = new HashSet<String>(); fieldSet.add("password"); // 獲取所有欄位:然後獲取這個類所有欄位 Field[] fields = obj.getClass().getDeclaredFields(); // 敏感欄位設定為空 for (Field field : fields) { if (fieldSet.contains(field.getName())) { // 開放欄位操作許可權 field.setAccessible(true); // 設定空 field.set(obj, null); } } // 返回物件 return obj; }}
在這個方法中,你把 Object 作為傳入引數,然後用反射操作欄位,把 password 設定為空。程式碼一氣呵成,於是你又寫出了下面的測試程式碼。
public class ApplicationV1 { // ...省略部分程式碼 public static void main(String[] args) throws Exception { // 初始化 ShopUser shopUser = new ShopUser(0L, "shopUser", "123456"); ClientUser clientUser = new ClientUser(0L, "clientUser", "123456"); // 輸出原始資訊 System.out.println("過濾前:"); System.out.println(" " + shopUser); System.out.println(" " + clientUser); // 執行過濾 shopUser = (ShopUser) removeField(shopUser); clientUser = (ClientUser) removeField(clientUser); // 輸出過濾後資訊 System.out.println("過濾後:"); System.out.println(" " + shopUser); System.out.println(" " + clientUser); }}執行結果過濾前: ShopUser{id=0, username='shopUser', password='123456'} ClientUser{id=0, username='clientUser', password='123456'}過濾後: ShopUser{id=null, username='shopUser', password='null'} ClientUser{id=null, username='clientUser', password='null'}
執行結果看起來沒問題,但很遺憾,這個方法不能用。最顯而易見的問題是,簡潔性不夠。這個方法要強制轉換物件,你看看這兩行測試程式碼:
// 執行過濾 shopUser = (ShopUser) removeField(shopUser); clientUser = (ClientUser) removeField(clientUser);
明明是同一個物件,你過濾掉敏感欄位後,自己還得再轉換一次物件。你想想看,這好歹是一個通用方法,要用在很多地方,當然是越簡單越好。
你又想到了,Java 有方法過載機制,你寫出了第二個版本。
public class ApplicationV2 { /********************** 業務方法 ************************/ public static ShopUser removeField(ShopUser user) throws Exception { // 強轉,並返回物件 return (ShopUser) remove(user); } public static ClientUser removeField(ClientUser user) throws Exception { // 強轉,並返回物件 return (ClientUser) remove(user); } /********************** 核心方法 ************************/ // 把敏感欄位設定為空 public static Object remove(Object obj) throws Exception { // 需要過濾的敏感欄位 Set<String> fieldSet = new HashSet<String>(); fieldSet.add("password"); // 獲取所有欄位:然後獲取這個類所有欄位 Field[] fields = obj.getClass().getDeclaredFields(); // 敏感欄位設定為空 for (Field field : fields) { if (fieldSet.contains(field.getName())) { // 開放欄位操作許可權 field.setAccessible(true); // 設定空 field.set(obj, null); } } // 返回物件 return obj; }}
這樣一來,問題好像又解決了。但新問題來了,重複方法特別多,而且如果再加一個供應商使用者,我還得再寫一個方法嗎?這可是通用方法,動不動就改原始碼,也不是辦法呀。
在沒有泛型的情況下,重複程式碼沒法解決,你總得做些沒意義的操作。要不強轉物件,要不就多寫幾個方法。
然而, Java 的 1.5 版本引入了泛型機制,程式碼可以變得更加簡單。 利用泛型,你寫出了第三個版本。
public class ApplicationV3 { // 把敏感欄位設定為空 public static <T> T removeField(T obj) throws Exception { // 需要過濾的敏感欄位 Set<String> fieldSet = new HashSet<String>(); fieldSet.add("password"); // 獲取所有欄位:然後獲取這個類所有欄位 Field[] fields = obj.getClass().getDeclaredFields(); // 敏感欄位設定為空 for (Field field : fields) { if (fieldSet.contains(field.getName())) { // 開放欄位操作許可權 field.setAccessible(true); // 設定空 field.set(obj, null); } } // 返回物件 return obj; }}
在第三個版本中,你使用了泛型,呼叫方法時不用強轉物件了,你也不用在原始碼寫這麼多重複方法,程式碼變得更加簡單了。
你再仔細看完上面的程式碼,可以發現, 泛型的使用步驟:定義型別變數 <T> 、使用型別變數 T obj 、確定型別變數 removeField(new ShopUser(0L, "shopUser", "123456")) 這點非常重要,這裡先按下不表。
這就是泛型,你不用把引數的型別寫死在程式碼,而是在使用的時候,再確定具體的型別。使用了泛型,你的程式碼可以變得更簡單、安全。
當然, 泛型很多的用法,分別是:泛型類及介面、泛型方法、萬用字元。 接下來,我們就一個個解鎖吧~
泛型類當泛型用在類和介面時,就被稱為泛型類、泛型介面。這個最典型的運用就是各種集合類和介面,比如,List、ArrayList 等等。
那麼,我們泛型怎麼用在類上面呢?
首先,定義一個泛型類。public class IdGen<T> { protected T id; public Generic(T id) { this.id = id; }}
IdGen 是一個 id 生成類。第一行程式碼中, <T> 是泛型標識,代表你定義了一個型別變數 T。第二行程式碼,我使用這個型別變數,把 id 定義成一個泛型。
然後,在例項化、繼承的的時候,指定具體的型別。public class IdGen<T> { // ..省略部分程式碼 // 透過繼承,確定泛型變數 static class User extends IdGen<Integer> { public User(Integer id) { super(id); } } public static void main(String[] args) { // 透過例項化,確定泛型變數 IdGen idGen = new IdGen<String>("1"); System.out.println(idGen); User user = new User(1); System.out.println(user); }}
使用者類繼承了 IdGen,在程式碼 extends IdGen<Integer> 中,指定了 Integer 作為 id 的具體型別;而 IdGen 例項化的時候,在程式碼 new IdGen<String>("1") 中,則指定了 String 作為 id 的具體型別。
泛型方法泛型不僅能用在類和介面上,還可以用在方法上。
比如,怎麼把一個類的成員變數轉換成 Map 集合呢?
這時候,我們可以寫一個泛型方法。
public class Generic { public static <T> Map obj2Map(T obj) throws Exception { Map map = new HashMap<>(); // 獲取所有欄位:透過 getClass() 方法獲取 Class 物件,然後獲取這個類所有欄位 Field[] fields = obj.getClass().getDeclaredFields(); for (Field field : fields) { // 開放欄位操作許可權 field.setAccessible(true); // 設定值 map.put(field.getName(), field.get(obj)); } return map; }}
同樣的, <T> 是泛型標識,代表你定義了一個型別變數 T,用在這個方法上。 T obj 使用型別變數 T,定義一個 obj 引數。最後,在呼叫方法的的時候,再確定具體的型別。
泛型萬用字元泛型萬用字元用 ? 表示,代表不確定的型別,是泛型的一個重要組成。
有一點很多文章都沒提到,大家一定要記住!!!
使用泛型有三個步驟:定義型別變數、使用型別變數、確定型別變數。在第三步,確定型別變數的時候,如果你沒法明確型別變數,這時候可以用泛型萬用字元。一般情況下,我們不需要用到泛型萬用字元,因為你能明確地知道型別變數,你看下面程式碼。
public class Application { public static Integer count(List<Integer> list) { int total = 0; for (Integer number : list) { total += number; } list.add(total); return total; } public static void main(String[] args) { // 不傳指定資料,編譯報錯 List<String> strList = Arrays.asList("0", "1", "2"); int totalNum = count(strList); // 繞過了編譯,執行報錯 List strList1 = Arrays.asList("0", "1", "2"); totalNum = count(strList1); }}
你非常清楚 count() 方法是幹什麼的,所以你在寫程式碼的時候,直接就能指明這是一個 Integer 集合。這樣一來,在呼叫方法的時候,如果不傳指定的資料進來,就沒法透過編譯。退一萬步講,即使你繞過了編譯這一關,程式也很可能沒法執行。
所以,如果你非常清楚自己要幹什麼,可以很明確地知道型別變數,那沒必要用泛型萬用字元。
然而,在一些通用方法中,什麼型別的資料都能傳進來,你沒法確認型別變數,這時候該怎麼辦呢?
你可以使用泛型萬用字元,這樣就不用確認型別變數,從而實現一些通用演算法。比如,你要寫一個通用方法,把傳入的 List 集合輸出到控制檯,那麼就可以這樣做。
public class Application { public static void print(List<?> list) { for (int i = 0; i < list.size(); i++) { System.out.println(list.get(i)); } } public static void main(String[] args) { // Integer 集合,可以執行 List<Integer> intList = Arrays.asList(0, 1, 2); print(intList); // String 集合,可以執行 List<String> strList = Arrays.asList("0", "1", "2"); print(strList); }}
List<?> list 代表我不確定 List 集合裝的是什麼型別,有可能是 Integer,有可能是 String,還可能是別的東西。但我不管這些,你只要傳一個 List 集合進來,這個方法就能正常執行。
這就是泛型萬用字元。 此外,有些演算法雖然也是通用的,但適用範圍不那麼大。 比如,使用者分為:普通使用者、商家使用者,但使用者有一些特殊功能,其它角色都沒有。這時候,又該怎麼辦呢?
你可以給泛型萬用字元設定邊界,以此限定型別變數的範圍。
泛型萬用字元的上邊界上邊界,代表型別變數的範圍有限,只能傳入某種型別,或者它的子類。你看下這幅圖就明白了。
利用 <? extends 類名> 的方式,可以設定泛型萬用字元的上邊界。你看下這個例子就明白了。
public class TopLine { public static void print(List<? extends Number> list) { for (int i = 0; i < list.size(); i++) { System.out.println(list.get(i)); } } public static void main(String[] args) { // Integer 是 Number 的子類,可以呼叫 print 方法 print(new ArrayList<Integer>()); // String 不是 Number 的子類,沒法呼叫 print 方法 print(new ArrayList<String>()); }}
你想呼叫 print() 方法中,那麼你可以傳入 Integer 集合,因為 Integer 是 Number 的子類。但 String 不是 Number 的子類,所以你沒法傳入 String 集合。
泛型萬用字元的下邊界下邊界,代表型別變數的範圍有限,只能傳入某種型別,或者它的父類。你看下這幅圖就明白了。
利用 <? super 類名> 的方式,可以設定泛型萬用字元的上邊界。你看下這個例子就明白了。
public class LowLine { public static void print(List<? super Integer> list) { for (int i = 0; i < list.size(); i++) { System.out.println(list.get(i)); } } public static void main(String[] args) { // Number 是 Integer 的父類,可以呼叫 print 方法 print(new ArrayList<Number>()); // Long 不是 Integer 的父類,沒法呼叫 print 方法 // print(new ArrayList<String>()); }}
你想呼叫 print() 方法中,那麼可以傳入 Number 集合,因為 Number 是 Integer 的父類。但 Long 不是 Integer 的父類,所以你沒法傳入 Long 集合。
寫在最後泛型是一種特殊的型別,你可以把泛型用在類、介面、方法上,從而實現一些通用演算法。
此外,使用泛型有三個步驟:定義型別變數、使用型別變數、確定型別變數。
在確定型別變數這一步中,你可以用泛型萬用字元來限制泛型的範圍,從而實現一些特殊演算法。