JAVA泛型詳解——轉

泛型(Generic type 或者generics)是對 Java 語言的類型系統的一種擴展,以支持建立能夠按類型進行參數化的類。能夠把類型參數看做是使用參數化類型時指定的類型的一個佔位符,就像方法的形式參數是運行時傳遞的值的佔位符同樣。  能夠在集合框架(Collection framework)中看到泛型的動機。例如,Map類容許您向一個Map添加任意類的對象,即便最多見的狀況是在給定映射(map)中保存某個特定類型(好比String)的對象。  由於Map.get()被定義爲返回Object,因此通常必須將Map.get()的結果強制類型轉換爲指望的類型,以下面的代碼所示:  Map m = new HashMap();  m.put("key", "blarg");  String s = (String) m.get("key");  要讓程序經過編譯,必須將get()的結果強制類型轉換爲String,而且但願結果然的是一個String。可是有可能某人已經在該映射中保存了不是String的東西,這樣的話,上面的代碼將會拋出ClassCastException。  理想狀況下,您可能會得出這樣一個觀點,即m是一個Map,它將String鍵映射到String值。這可讓您消除代碼中的強制類型轉換,同時得到一個附加的類型檢查層,該檢查層能夠防止有人將錯誤類型的鍵或值保存在集合中。這就是泛型所作的工做。  泛型的好處  Java 語言中引入泛型是一個較大的功能加強。不只語言、類型系統和編譯器有了較大的變化,以支持泛型,並且類庫也進行了大翻修,因此許多重要的類,好比集合框架,都已經成爲泛型化的了。這帶來了不少好處:  · 類型安全。泛型的主要目標是提升 Java 程序的類型安全。經過知道使用泛型定義的變量的類型限制,編譯器能夠在一個高得多的程度上驗證類型假設。沒有泛型,這些假設就只存在於程序員的頭腦中(或者若是幸運的話,還存在於代碼註釋中)。  Java 程序中的一種流行技術是定義這樣的集合,即它的元素或鍵是公共類型的,好比「String列表」或者「String到String的映射」。經過在變量聲明中捕獲這一附加的類型信息,泛型容許編譯器實施這些附加的類型約束。類型錯誤如今就能夠在編譯時被捕獲了,而不是在運行時看成ClassCastException展現出來。將類型檢查從運行時挪到編譯時有助於您更容易找到錯誤,並可提升程序的可靠性。  · 消除強制類型轉換。泛型的一個附帶好處是,消除源代碼中的許多強制類型轉換。這使得代碼更加可讀,而且減小了出錯機會。  儘管減小強制類型轉換能夠下降使用泛型類的代碼的羅嗦程度,可是聲明泛型變量會帶來相應的羅嗦。比較下面兩個代碼例子。  該代碼不使用泛型:  List li = new ArrayList();  li.put(new Integer(3));  Integer i = (Integer) li.get(0);  該代碼使用泛型:  List<Integer> li = new ArrayList<Integer>();  li.put(new Integer(3));  Integer i = li.get(0);  在簡單的程序中使用一次泛型變量不會下降羅嗦程度。可是對於屢次使用泛型變量的大型程序來講,則能夠累積起來下降羅嗦程度。  · 潛在的性能收益。泛型爲較大的優化帶來可能。在泛型的初始實現中,編譯器將強制類型轉換(沒有泛型的話,程序員會指定這些強制類型轉換)插入生成的字節碼中。可是更多類型信息可用於編譯器這一事實,爲將來版本的JVM 的優化帶來可能。  因爲泛型的實現方式,支持泛型(幾乎)不須要JVM 或類文件更改。全部工做都在編譯器中完成,編譯器生成相似於沒有泛型(和強制類型轉換)時所寫的代碼,只是更能確保類型安全而已。  泛型用法的例子  泛型的許多最佳例子都來自集合框架,由於泛型讓您在保存在集合中的元素上指定類型約束。考慮這個使用Map類的例子,其中涉及必定程度的優化,即Map.get()返回的結果將確實是一個String:  Map m = new HashMap();  m.put("key", "blarg");  String s = (String) m.get("key");  若是有人已經在映射中放置了不是String的其餘東西,上面的代碼將會拋出ClassCastException。泛型容許您表達這樣的類型約束,即m是一個將String鍵映射到String值的Map。這能夠消除代碼中的強制類型轉換,同時得到一個附加的類型檢查層,這個檢查層能夠防止有人將錯誤類型的鍵或值保存在集合中。  下面的代碼示例展現了 JDK 5.0 中集合框架中的Map接口的定義的一部分:  public interface Map<K, V> {  public void put(K key, V value);  public V get(K key);  }  注意該接口的兩個附加物:  * 類型參數 K 和 V 在類級別的規格說明,表示在聲明一個 Map 類型的變量時指定的類型的佔位符。  * 在 get()、put() 和其餘方法的方法簽名中使用的 K 和 V。  爲了贏得使用泛型的好處,必須在定義或實例化Map類型的變量時爲K和V提供具體的值。以一種相對直觀的方式作這件事:  Map<String, String> m = new HashMap<String, String>();  m.put("key", "blarg");  String s = m.get("key");  當使用Map的泛型化版本時,您再也不須要將Map.get()的結果強制類型轉換爲String,由於編譯器知道get()將返回一個String。  在使用泛型的版本中並無減小鍵盤錄入;實際上,比使用強制類型轉換的版本須要作更多鍵入。使用泛型只是帶來了附加的類型安全。由於編譯器知道關於您將放進Map中的鍵和值的類型的更多信息,因此類型檢查從執行時挪到了編譯時,這會提升可靠性並加快開發速度。  向後兼容  在 Java 語言中引入泛型的一個重要目標就是維護向後兼容。儘管 JDK 5.0 的標準類庫中的許多類,好比集合框架,都已經泛型化了,可是使用集合類(好比HashMap和ArrayList)的現有代碼將繼續不加修改地在 JDK 5.0 中工做。固然,沒有利用泛型的現有代碼將不會贏得泛型的類型安全好處。  類型參數  在定義泛型類或聲明泛型類的變量時,使用尖括號來指定形式類型參數。形式類型參數與實際類型參數之間的關係相似於形式方法參數與實際方法參數之間的關係,只是類型參數表示類型,而不是表示值。  泛型類中的類型參數幾乎能夠用於任何可使用類名的地方。例如,下面是java.util.Map接口的定義的摘錄:  public interface Map<K, V> {  public void put(K key, V value);  public V get(K key);  }  Map接口是由兩個類型參數化的,這兩個類型是鍵類型K和值類型V。(不使用泛型)將會接受或返回Object的方法如今在它們的方法簽名中使用K或V,指示附加的類型約束位於Map的規格說明之下。  當聲明或者實例化一個泛型的對象時,必須指定類型參數的值:  Map<String, String> map = new HashMap<String, String>();  注意,在本例中,必須指定兩次類型參數。一次是在聲明變量map的類型時,另外一次是在選擇HashMap類的參數化以即可以實例化正確類型的一個實例時。  編譯器在遇到一個Map<String, String>類型的變量時,知道K和V如今被綁定爲String,所以它知道在這樣的變量上調用Map.get()將會獲得String類型。  除了異常類型、枚舉或匿名內部類之外,任何類均可以具備類型參數。  命名類型參數  推薦的命名約定是使用大寫的單個字母名稱做爲類型參數。這與C++ 約定有所不一樣(參閱附錄 A:與 C++ 模板的比較),並反映了大多數泛型類將具備少許類型參數的假定。對於常見的泛型模式,推薦的名稱是:  * K —— 鍵,好比映射的鍵。  * V —— 值,好比 List 和 Set 的內容,或者 Map 中的值。  * E —— 異常類。  * T —— 泛型。  泛型不是協變的  關於泛型的混淆,一個常見的來源就是假設它們像數組同樣是協變的。其實它們不是協變的。List<Object>不是List<String>的父類型。  若是 A 擴展 B,那麼 A 的數組也是 B 的數組,而且徹底能夠在須要B[]的地方使用A[]:  Integer[] intArray = new Integer[10];  Number[] numberArray = intArray;  上面的代碼是有效的,由於一個Integer是一個Number,於是一個Integer數組是一個Number數組。可是對於泛型來講則否則。下面的代碼是無效的:  List<Integer> intList = new ArrayList<Integer>();  List<Number> numberList = intList; // invalid  最初,大多數 Java 程序員以爲這缺乏協變很煩人,或者甚至是「壞的(broken)」,可是之因此這樣有一個很好的緣由。若是能夠將List<Integer>賦給List<Number>,下面的代碼就會違背泛型應該提供的類型安全:  List<Integer> intList = new ArrayList<Integer>();  List<Number> numberList = intList; // invalid  numberList.add(new Float(3.1415));  由於intList和numberList都是有別名的,若是容許的話,上面的代碼就會讓您將不是Integers的東西放進intList中。可是,正以下一屏將會看到的,您有一個更加靈活的方式來定義泛型。  package com.ibm.course.generics;  import java.util.ArrayList;  import java.util.List;  public class GenericsExample {  public static void main(String[] args) {  Integer[] integer = new Integer[5];  Number[] number = integer;  System.out.println(number[0]);// null  number[0] = new Float(7.65);  System.out.println(number[0]);  System.out.println(integer[0]);  List<Integer> list = new ArrayList<Integer>();  // Type mismatch: cannot convert from List<Integer> to List<Number>  // List<Number> listObj = list;  }  }  List<Number> listObj = list;致使編譯錯誤:Type mismatch: cannot convert from List<Integer> to List<Number>  而System.out.println(number[0]);和System.out.println(integer[0]);致使運行時異常:  Exception in thread "main" java.lang.ArrayStoreException: java.lang.Float  at com.ibm.course.generics.GenericsExample.main(GenericsExample.java:15)  類型通配符  假設您具備該方法:  void printList(List l) {  for (Object o : l)  System.out.println(o);  }  上面的代碼在 JDK 5.0 上編譯經過,可是若是試圖用List<Integer>調用它,則會獲得警告。出現警告是由於,您將泛型(List<Integer>)傳遞給一個只承諾將它看成List(所謂的原始類型)的方法,這將破壞使用泛型的類型安全。  若是試圖編寫像下面這樣的方法,那麼將會怎麼樣?  void printList(List<Object> l) {  for (Object o : l)  System.out.println(o);  }  它仍然不會經過編譯,由於一個List<Integer>不是一個List<Object>(正如前一屏泛型不是協變的 中所學的)。這才真正煩人——如今您的泛型版本尚未普通的非泛型版本有用!  解決方案是使用類型通配符:  void printList(List<?> l) {  for (Object o : l)  System.out.println(o);  }  上面代碼中的問號是一個類型通配符。它讀做「問號」。List<?>是任何泛型List的父類型,因此您徹底能夠將List<Object>、List<Integer>或List<List<List<Flutzpah>>>傳遞給printList()。  package com.ibm.course.generics;  import java.util.ArrayList;  import java.util.List;  public class GenericExample {  public static void main(String[] args) {  List<Integer> integer = new ArrayList<Integer>();  integer.add(new Integer(0));  integer.add(new Integer(1));  List<String> str = new ArrayList<String>();  str.add(new String("Hello"));  str.add(new String("World"));  List<?> li=integer;  li=str;  printList(integer);  printList(str);  }  public static void printList(List<?> l) {  for (Object o : l) {  System.out.println(o);  }  }  }  上面的例子程序沒有警告也沒有編譯錯誤。  類型通配符的做用  前一屏類型通配符 中引入了類型通配符,這讓您能夠聲明List<?>類型的變量。您能夠對這樣的List作什麼呢?很是方便,能夠從中檢索元素,可是不能添加元素(能夠添加null)。緣由不是編譯器知道哪些方法修改列表哪些方法不修改列表,而是(大多數)變化的方法比不變化的方法須要更多的類型信息。下面的代碼則工做得很好:  List<Integer> li = new ArrayList<Integer>();  li.add(new Integer(42));  List<?> lu = li;  System.out.println(lu.get(0));  爲何該代碼能工做呢?對於lu,編譯器一點都不知道List的類型參數的值。可是編譯器比較聰明,它能夠作一些類型推理。在本例中,它推斷未知的類型參數必須擴展Object。(這個特定的推理沒有太大的跳躍,可是編譯器能夠做出一些很是使人佩服的類型推理,後面就會看到(在底層細節 一節中)。因此它讓您調用List.get()並推斷返回類型爲Object。  另外一方面,下面的代碼不能工做:  List<Integer> li = new ArrayList<Integer>();  li.add(new Integer(42));  List<?> lu = li;  lu.add(new Integer(43)); // error  在本例中,對於lu,編譯器不能對List的類型參數做出足夠嚴密的推理,以肯定將Integer傳遞給List.add()是類型安全的。因此編譯器將不容許您這麼作。  以避免您仍然認爲編譯器知道哪些方法更改列表的內容哪些不更改列表內容,請注意下面的代碼將能工做,由於它不依賴於編譯器必須知道關於lu的類型參數的任何信息:  List<Integer> li = new ArrayList<Integer>();  li.add(new Integer(42));  List<?> lu = li;  lu.clear();  泛型方法  (在類型參數 一節中)您已經看到,經過在類的定義中添加一個形式類型參數列表,能夠將類泛型化。方法也能夠被泛型化,無論它們定義在其中的類是否是泛型化的。  泛型類在多個方法簽名間實施類型約束。在List<V>中,類型參數V出如今get()、add()、contains()等方法的簽名中。當建立一個Map<K, V>類型的變量時,您就在方法之間宣稱一個類型約束。您傳遞給add()的值將與get()返回的值的類型相同。  相似地,之因此聲明泛型方法,通常是由於您想要在該方法的多個參數之間宣稱一個類型約束。例如,下面代碼中的ifThenElse()方法,根據它的第一個參數的布爾值,它將返回第二個或第三個參數:  public <T> T ifThenElse(boolean b, T first, T second) {  return b ? first : second;  }  注意,您能夠調用ifThenElse(),而不用顯式地告訴編譯器,您想要T的什麼值。編譯器沒必要顯式地被告知 T 將具備什麼值;它只知道這些值都必須相同。編譯器容許您調用下面的代碼,由於編譯器可使用類型推理來推斷出,替代T的String知足全部的類型約束:  String s = ifThenElse(b, "a", "b");  相似地,您能夠調用:  Integer i = ifThenElse(b, new Integer(1), new Integer(2));  可是,編譯器不容許下面的代碼,由於沒有類型會知足所需的類型約束:  String s = ifThenElse(b, "pi", new Float(3.14));  爲何您選擇使用泛型方法,而不是將類型T添加到類定義呢?(至少)有兩種狀況應該這樣作:  * 當泛型方法是靜態的時,這種狀況下不能使用類類型參數。  * 當 T 上的類型約束對於方法真正是局部的時,這意味着沒有在相同類的另外一個 方法簽名中使用相同 類型 T 的約束。經過使得泛型方法的類型參數對於方法是局部的,能夠簡化封閉類型的簽名。  有限制類型  在前一屏泛型方法 的例子中,類型參數V是無約束的或無限制的類型。有時在尚未徹底指定類型參數時,須要對類型參數指定附加的約束。  考慮例子Matrix類,它使用類型參數V,該參數由Number類來限制:  public class Matrix<V extends Number> { ... }  編譯器容許您建立Matrix<Integer>或Matrix<Float>類型的變量,可是若是您試圖定義Matrix<String>類型的變量,則會出現錯誤。類型參數V被判斷爲由Number限制。在沒有類型限制時,假設類型參數由Object限制。這就是爲何前一屏泛型方法 中的例子,容許List.get()在List<?>上調用時返回Object,即便編譯器不知道類型參數V的類型。
相關文章
相關標籤/搜索