瘋狂Java學習筆記(69)---------Lock">瘋狂Java學習筆記(69)---------Lock
753
2025-03-31
瘋狂Java學習筆記(48)------------泛型詳解
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
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
public void put(K key, V value);
public V get(K key);
}
注意該接口的兩個附加物:
類型參數 K 和 V 在類級別的規格說明,表示在聲明一個 Map 類型的變量時指定的類型的占位符。
在 get()、put() 和其他方法的方法簽名中使用的 K 和 V。
為了贏得使用泛型的好處,必須在定義或實例化 Map 類型的變量時為 K 和 V 提供具體的值。以一種相對直觀的方式做這件事:
Map
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
public void put(K key, V value);
public V get(K key);
}
Map 接口是由兩個類型參數化的,這兩個類型是鍵類型 K 和值類型 V。(不使用泛型)將會接受或返回 Object 的方法現在在它們的方法簽名中使用 K 或 V,指示附加的類型約束位于 Map 的規格說明之下。
當聲明或者實例化一個泛型的對象時,必須指定類型參數的值:
Map
注意,在本例中,必須指定兩次類型參數。一次是在聲明變量 map 的類型時,另一次是在選擇 HashMap 類的參數化以便可以實例化正確類型的一個實例時。
編譯器在遇到一個 Map
除了異常類型、枚舉或匿名內部類以外,任何類都可以具有類型參數。
命名類型參數
推薦的命名約定是使用大寫的單個字母名稱作為類型參數。這與 C++ 約定有所不同(參閱 附錄 A:與 C++ 模板的比較),并反映了大多數泛型類將具有少量類型參數的假定。對于常見的泛型模式,推薦的名稱是:
K —— 鍵,比如映射的鍵。
V —— 值,比如 List 和 Set 的內容,或者 Map 中的值。
E —— 異常類。
T ——?泛型。
泛型不是協變的
關于泛型的混淆,一個常見的來源就是假設它們像數組一樣是協變的。其實它們不是協變的。List
如果 A 擴展 B,那么 A 的數組也是 B 的數組,并且完全可以在需要 B[] 的地方使用 A[]:
Integer[] intArray = new Integer[10];
Number[] numberArray = intArray;
上面的代碼是有效的,因為一個 Integer 是 一個 Number,因而一個 Integer 數組是 一個 Number 數組。但是對于泛型來說則不然。下面的代碼是無效的:
List
List
最初,大多數?Java?程序員覺得這缺少協變很煩人,或者甚至是“壞的(broken)”,但是之所以這樣有一個很好的原因。如果可以將 List
List
List
numberList.add(new Float(3.1415));
因為 intList 和 numberList 都是有別名的,如果允許的話,上面的代碼就會讓您將不是 Integers 的東西放進 intList 中。但是,正如下一屏將會看到的,您有一個更加靈活的方式來定義泛型。
類型通配符
假設您具有該方法:
void printList(List l) {
for (Object o : l)
System.out.println(o);
}
上面的代碼在 JDK 5.0 上編譯通過,但是如果試圖用 List
如果試圖編寫像下面這樣的方法,那么將會怎么樣?
void printList(List
for (Object o : l)
System.out.println(o);
}
它仍然不會通過編譯,因為一個 List
解決方案是使用類型通配符:
void printList(List> l) {
for (Object o : l)
System.out.println(o);
}
上面代碼中的問號是一個類型通配符。它讀作“問號”。List> 是任何泛型?List 的父類型,所以您完全可以將 List
類型通配符的作用
前一屏 類型通配符 中引入了類型通配符,這讓您可以聲明 List> 類型的變量。您可以對這樣的 List 做什么呢?非常方便,可以從中檢索元素,但是不能添加元素。原因不是編譯器知道哪些方法修改列表哪些方法不修改列表,而是(大多數)變化的方法比不變化的方法需要更多的類型信息。下面的代碼則工作得很好:
List
li.add(new Integer(42));
List> lu = li;
System.out.println(lu.get(0));
為什么該代碼能工作呢?對于 lu,編譯器一點都不知道 List 的類型參數的值。但是編譯器比較聰明,它可以做一些類型推理。在本例中,它推斷未知的類型參數必須擴展 Object。(這個特定的推理沒有太大的跳躍,但是編譯器可以作出一些非常令人佩服的類型推理,后面就會看到(在 底層細節 一節中)。所以它讓您調用 List.get() 并推斷返回類型為 Object。
另一方面,下面的代碼不能工作:
List
li.add(new Integer(42));
List> lu = li;
lu.add(new Integer(43)); // error
在本例中,對于 lu,編譯器不能對 List 的類型參數作出足夠嚴密的推理,以確定將 Integer 傳遞給 List.add() 是類型安全的。所以編譯器將不允許您這么做。
以免您仍然認為編譯器知道哪些方法更改列表的內容哪些不更改列表內容,請注意下面的代碼將能工作,因為它不依賴于編譯器必須知道關于 lu 的類型參數的任何信息:
List
li.add(new Integer(42));
List> lu = li;
lu.clear();
泛型方法
(在 類型參數 一節中)您已經看到,通過在類的定義中添加一個形式類型參數列表,可以將類泛型化。方法也可以被泛型化,不管它們定義在其中的類是不是泛型化的。
泛型類在多個方法簽名間實施類型約束。在 List
類似地,之所以聲明泛型方法,一般是因為您想要在該方法的多個參數之間宣稱一個類型約束。例如,下面代碼中的 ifThenElse() 方法,根據它的第一個參數的布爾值,它將返回第二個或第三個參數:
public
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
編譯器允許您創建 Matrix
三 一個簡單的泛型類
編寫基本的容器類
此時,您可以開始編寫簡單的泛型類了。到目前為止,泛型類最常見的用例是容器類(比如集合框架)或者值持有者類(比如 WeakReference 或 ThreadLocal)。我們來編寫一個類,它類似于 List,充當一個容器。其中,我們使用泛型來表示這樣一個約束,即 Lhist 的所有元素將具有相同類型。為了實現起來簡單,Lhist 使用一個固定大小的數組來保存值,并且不接受 null 值。
Lhist 類將具有一個類型參數 V(該參數是 Lhist 中的值的類型),并將具有以下方法:
public class Lhist
public Lhist(int capacity) { ... }
public int size() { ... }
public void add(V value) { ... }
public void remove(V value) { ... }
public V get(int index) { ... }
}
要實例化 Lhist,只要在聲明時指定類型參數和想要的容量:
Lhist
實現構造函數
在實現 Lhist 類時,您將會遇到的第一個攔路石是實現構造函數。您可能會像下面這樣實現它:
public class Lhist
private V[] array;
public Lhist(int capacity) {
array = new V[capacity]; // illegal
}
}
這似乎是分配后備數組最自然的一種方式,但是不幸的是,您不能這樣做。具體原因很復雜,當學習到 底層細節 一節中的“擦除”主題時,您就會明白。分配后備數組的實現方式很古怪且違反直覺。下面是構造函數的一種可能的實現(該實現使用集合類所采用的方法):
public class Lhist
private V[] array;
public Lhist(int capacity) {
array = (V[]) new Object[capacity];
}
}
另外,也可以使用反射來實例化數組。但是這樣做需要給構造函數傳遞一個附加的參數 —— 一個類常量,比如 Foo.class。后面在 Class
實現方法
實現 Lhist 的方法要容易得多。下面是 Lhist 類的完整實現:
public class Lhist
private V[] array;
private int size;
public Lhist(int capacity) {
array = (V[]) new Object[capacity];
}
public void add(V value) {
if (size == array.length)
throw new IndexOutOfBoundsException(Integer.toString(size));
else if (value == null)
throw new NullPointerException();
array[size++] = value;
}
public void remove(V value) {
int removalCount = 0;
for (int i=0; i if (array[i].equals(value)) ++removalCount; else if (removalCount > 0) { array[i-removalCount] = array[i]; array[i] = null; } } size -= removalCount; } public int size() { return size; } public V get(int i) { if (i >= size) throw new IndexOutOfBoundsException(Integer.toString(i)); return array[i]; } } 注意,您在將會接受或返回 V 的方法中使用了形式類型參數 V,但是您一點也不知道 V 具有什么樣的方法或域,因為這些對泛型代碼是不可知的。 使用 Lhist 類 使用 Lhist 類很容易。要定義一個整數 Lhist,只需要在聲明和構造函數中為類型參數提供一個實際值即可: Lhist 編譯器知道,li.get() 返回的任何值都將是 Integer 類型,并且它還強制傳遞給 li.add() 或 li.remove() 的任何東西都是 Integer。除了實現構造函數的方式很古怪之外,您不需要做任何十分特殊的事情以使 Lhist 是一個泛型類。 四?Java類庫中的泛型 集合類 到目前為止,Java?類庫中泛型支持存在最多的地方就是集合框架。就像容器類是 C++ 語言中模板的主要動機一樣(參閱 附錄 A:與 C++ 模板的比較)(盡管它們隨后用于很多別的用途),改善集合類的類型安全是?Java?語言中泛型的主要動機。集合類也充當如何使用泛型的模型,因為它們演示了泛型的幾乎所有的標準技巧和方言。 所有的標準集合接口都是泛型化的 —— Collection 集合類也使用泛型的許多“技巧”和方言,比如上限通配符和下限通配符。例如,在接口 Collection interface Collection boolean addAll(Collection extends V>); } 該定義組合了通配符類型參數和有限制類型參數,允許您將 Collection 如果類庫將 addAll() 定義為接受 Collection 應該可以將 addAll() 的類型參數定義為 Collection 作為泛型化一個類(如果不小心的話)如何會更改其語義的一個更加微妙的例子,注意 Collection.removeAll() 的參數的類型是 Collection>,而不是 Collection extends V>。這是因為傳遞混合類型的集合給 removeAll() 是可接受的,并且更加限制地定義 removeAll 將會更改方法的語義和有用性。 其他容器類 除了集合類之外,Java?類庫中還有幾個其他的類也充當值的容器。這些類包括 WeakReference、SoftReference 和 ThreadLocal。它們都已經在其包含的值的類型上泛型化了,所以 WeakReference 泛型不止用于容器 泛型最常見最直觀的使用是容器類,比如集合類或引用類(比如 WeakReference 像 Comparable Comparable Comparable 接口已經泛型化了,所以實現 Comparable 的對象聲明它可以與什么類型進行比較。(通常,這是對象本身的類型,但是有時也可能是父類。) public interface Comparable public boolean compareTo(T other); } 所以 Comparable 接口包含一個類型參數 T,該參數是一個實現 Comparable 的類可以與之比較的對象的類型。這意味著如果定義一個實現 Comparable 的類,比如 String,就必須不僅聲明類支持比較,還要聲明它可與什么比較(通常是與它本身比較): public class String implements Comparable 現在來考慮一個二元 max() 方法的實現。您想要接受兩個相同類型的參數,二者都是 Comparable,并且相互之間是 Comparable。幸運的是,如果使用泛型方法和有限制類型參數的話,這相當直觀: public static if (t1.compareTo(t2) > 0) return t1; else return t2; } 在本例中,您定義了一個泛型方法,在類型 T 上泛型化,您約束該類型擴展(實現) Comparable 更好的是,編譯器將使用類型推理來確定當調用 max() 時 T 的值表示什么意思。所以根本不用指定 T,下面的調用就能工作: String s = max("moo", "bark"); 編譯器將計算出 T 的預定值是 String,因此它將進行編譯和類型檢查。但是如果您試圖用不實現 Comparable Class 類 Class 已經泛型化了,但是很多人一開始都感覺其泛型化的方式很混亂。Class 在以前的 JDK 中,Class.newInstance() 方法的定義返回 Object,您很可能要將該返回類型強制轉換為另一種類型: class Class { Object newInstance(); } 但是使用泛型,您定義 Class.newInstance() 方法具有一個更加特定的返回類型: class Class T newInstance(); } 如何創建一個 Class 讓 Foo.class 是 Class 考慮一個方法,它從數據庫檢索一組對象,并返回 JavaBeans 對象的一個集合。您通過反射來實例化和初始化創建的對象,但是這并不意味著類型安全必須完全被拋至腦后。考慮下面這個方法: public static // Use Selector to select rows List for (/* iterate over results */) { T row = c.newInstance(); // use reflection to set fields from result list.add(row); } return list; } 可以像下面這樣簡單地調用該方法: List 編譯器將會根據 FooRecord.class 是 Class 用 Class Collection 接口包含一個方法,用于將集合的內容復制到一個調用者指定類型的數組中: public Object[] toArray(Object[] prototypeArray) { ... } toArray(Object[]) 的語義是,如果傳遞的數組足夠大,就會使用它來保存結果,否則,就會使用反射分配一個相同類型的新數組。一般來說,單獨傳遞一個數組作為參數來提供想要的返回類型是一個小技巧,但是在引入泛型之前,這是與方法交流類型信息最方便的方式。 有了泛型,就可以用一種更加直觀的方式來做這件事。不像上面這樣定義 toArray(),泛型?toArray() 可能看起來像下面這樣: public 調用這樣一個 toArray() 方法很簡單: FooBar[] fba = something.toArray(FooBar.class); Collection 接口還沒有改變為使用該技術,因為這會破壞許多現有的集合實現。但是如果使用泛型從新構建 Collection,則當然會使用該方言來指定它想要返回值是哪種類型。 Enum JDK 5.0 中?Java?語言另一個增加的特性是枚舉。當您使用 enum 關鍵字聲明一個枚舉時,編譯器就會在內部為您生成一個類,用于擴展 Enum 并為枚舉的每個值聲明靜態實例。所以如果您說: public enum Suit {HEART, DIAMOND, CLUB, SPADE}; 編譯器就會在內部生成一個叫做 Suit 的類,該類擴展?java.lang.Enum 與 Class 一樣,Enum 也是一個泛型類。但是與 Class 不同,它的簽名稍微更復雜一些: class Enum 這究竟是什么意思?這難道不會導致無限遞歸? 我們逐步來分析。類型參數 E 用于 Enum 的各種方法中,比如 compareTo() 或 getDeclaringClass()。為了這些方法的類型安全,Enum 類必須在枚舉的類上泛型化。 所以 extends Enum 總之,Enum 是一個參數化的類型,只可以為它的子類型實例化,并且這些子類型然后將根據子類型來繼承方法。幸運的是,在 Enum 情況下,編譯器為您做這些工作,一切都很好。 與非泛型代碼相互操作 數百萬行現有代碼使用已經泛型化的?Java?類庫中的類,比如集合框架、Class 和 ThreadLocal。JDK 5.0 中的改進不要破壞所有這些代碼是很重要的,所以編譯器允許您在不指定其類型參數的情況下使用泛型類。 當然,以“舊方式”做事沒有新方式安全,因為忽略了編譯器準備提供的類型安全。如果您試圖將 List 沒有類型參數的泛型,比如聲明為 List 類型而不是 List 為了消除一些 unchecked-conversion 警告,假設您不準備泛型化所有的代碼,您可以使用通配符類型參數。使用 List> 而不使用 List。List 是原始類型;List> 是具有未知類型參數的泛型。編譯器將以不同的方式對待它們,并很可能發出更少的警告。 無論在哪種情況下,編譯器在生成字節碼時都會生成強制類型轉換,所以生成的字節碼在每種情況下都不會比沒有泛型時更不安全。如果您設法通過使用原始類型或類文件來破壞類型安全,就會得到與不使用泛型時得到的相同的 ClassCastException 或 ArrayStoreException。 已檢查集合 作為從原始集合類型遷移到泛型集合類型的幫助,集合框架添加了一些新的集合包裝器,以便為一些類型安全 bug 提供早期警告。就像 Collections.unmodifiableSet() 工廠方法用一個不允許任何修改的 Set 包裝一個現有 Set 一樣,Collections.checkedSet()(以及 checkedList() 和 checkedMap())工廠方法創建一個包裝器(或者視圖)類,以防止您將錯誤類型的變量放在集合中。 checkedXxx() 方法都接受一個類常量作為參數,所以它們可以(在運行時)檢查這些修改是允許的。典型的實現可能像下面這樣: public class Collections { public static return new CheckedCollection } private static class CheckedCollection private final Collection private final Class CheckedCollection(Collection this.c = c; this.type = type; } public boolean add(E o) { if (!type.isInstance(o)) throw new ClassCastException(); else return c.add(o); } } } 轉載自:https://blog.csdn.net/u011225629/article/details/45914551 Java 數據結構
版權聲明:本文內容由網絡用戶投稿,版權歸原作者所有,本站不擁有其著作權,亦不承擔相應法律責任。如果您發現本站中有涉嫌抄襲或描述失實的內容,請聯系我們jiasou666@gmail.com 處理,核實后本網站將在24小時內刪除侵權內容。
版權聲明:本文內容由網絡用戶投稿,版權歸原作者所有,本站不擁有其著作權,亦不承擔相應法律責任。如果您發現本站中有涉嫌抄襲或描述失實的內容,請聯系我們jiasou666@gmail.com 處理,核實后本網站將在24小時內刪除侵權內容。