一、基礎知識
1、類型擦除
類型擦除指的是通過類型參數合併, 將泛型類型實例關聯到同一份字節碼上。 編譯器只爲泛型類型生成一份字節碼, 並將其實例關聯到這份字節碼上, 因此泛型類型中的靜態變量是所有實例共享的。
(1) 一個static 方法, 無法訪問泛型類的類型參數, 因爲類還沒有實例化, 所以, 若static方法需要使用泛型能力, 必須使其成爲泛型方法,(泛型參數稍後會介紹),即沒有成爲泛型方法的靜態方法不能訪問類型參數。
(2) 泛型類的靜態上下文中類型變量無效,即靜態變量不能引用類型變量。
public class Singleton<T>{
private static T singleInstance;//錯誤,違反第二條
public static T getSingleInstance() // 錯誤,違反第一條
{
if(singleInstance == null)
Return singleInstance;
}
}
(3) 在使用泛型時, 任何具體的類型都被擦除, 唯一知道的是你在使用一個對象。 比如:List<String>和List<Integer>在運行事實上是相同的類型。 他們都被擦除成他們的原生類型,即List。 因爲編譯的時候會有類型擦除, 所以不能通過同一個泛型類的實例來區分方法, 如下面的例子編譯時會出錯, 因爲類型擦除後, 兩個方法都是List 類型的參數,
因此並不能
根據泛型類的類型來區分方法。
/*會導致編譯時錯誤*/
public class Erasure{
public void test(List<String> ls){
System.out.println("Sting");
}
public void test(List<Integer> li){
System.out.println("Integer");
}
}
(4) 所有泛型類的實例都共享同一個運行時類,
類型參數信息會在編譯時被擦除。 因此考慮如下代碼, 雖然 ArrayList<String>和 ArrayList<Integer>類型參數不同, 但是他們都共享
ArrayList 類, 所以結果會是 true。
List<String>l1 = new ArrayList<String>();
List<Integer>l2 = new ArrayList<Integer>();
System.out.println(l1.getClass() == l2.getClass()); //True
(5) 不能對確切的泛型類型使用instanceOf 操作。因爲instanceof會在運行時檢測對象的類型,而泛型在運行時已經被擦除了,所以所有的List都是一樣的,set也都是一樣的。如下面的操作是非法的, 編譯時會出錯。
Collection cs = new ArrayList<String>();
if (cs instanceof Collection<String>){…}// compile error.如果改成instanceof Collection<?>則不會出錯。
(6)
不能用基本類型實例化類型參數, 因此嗎, 沒有 Pair<double>,只有Pair<Double>,原因是類型擦除, 擦除後, Pair類含有Object類型的域, 而Object不能存儲doble 值。
2、泛型和子類型
List<Apple> apples = new ArrayList<Apple>(); //right
List<Fruit> fruits = apples; //error
我們假定第2行代碼沒有問題,
那麼我們可以使用語句fruits.add(new Strawberry())( Strawberry 爲 Fruit的子類) 在fruits中加入草莓了, 但是這樣的話, 一個List中裝入了
各種不同類型的子類水果, 這顯然是不可以的, 因爲我們在取出 List中的水果對象時, 就
分不清楚到底該轉型爲蘋果還是草莓了。
通常來說,如果 Foo是Bar的子類型,G是一種帶泛型的類型,則G<Foo>不是G<Bar>
的子類型。
3、通配符
(1)<?>非限定通配符
//使用通配符? , 表示可以接收任何元素類型的集合作爲參數
public void printCollection(Collection<?> c) {
for (Object e:c) {
System.out.println(e);
}
}
這裏使用了通配符? 指定可以使用任何類型的集合作爲參數。 讀取的元素使用了 Object 類型來表示, 這是安全的, 因爲所有的類都是 Object 的子類。 這裏就又出現了另外一個問題,如下代碼所示, 如果試圖往使用通配符? 的集合中加入對象, 就會在編譯時出現錯誤。 需要注意的是, 這裏不管加入什麼類型的對象都會出錯。這是因爲通配符?
表示該集合存儲的元素類型未知, 可以是任何類型。 往集合中加入元素需要是一個未知元素類型的子類型, 正因爲該集合存儲的元素類型未知, 所以我們沒法向該集合中添加任何元素。唯一的例外是null,因爲null是所有類型的子類型, 所以儘管元素類型不知道, 但是null一定是它的子類型。Collection<?> c=new ArrayList<String>();
c.add(newObject()); //compile time error, 不管加入什麼對象都出錯, 除了 null 外。
c.add(null); //OK
(2)限制性通配符:<? extends A>從一個數據類型裏獲取數據
<? super B>把對象寫入一個數據結構裏
假定有一個畫圖的應用, 可以畫各種形狀的圖形, 如矩形和圓形等。 爲了在程序裏面表示,
定義如下的類層次:
public abstract class Shape {
public abstract void draw(Canvas c);
}
public class Circle extends Shape {
private int x,y,radius;
public void draw(Canvas c) { ... }
}
public class Rectangle extends Shape
private int x,y,width,height;
public void draw(Canvas c) { ... }
}
如果我們希望在List<?exends Shape> shapes 中加入一個矩形對象, 如下所示:
shapes.add(0, new Rectangle()); //compile-time error
那麼這時會出現一個編譯時錯誤,
原因在於: 我們只知道 shapes
中的元素時 Shape類型的子類型, 具體是什麼子類型我們並不清楚, 所以我們不能往shapes中加入任何類型的對象。不過我們在取出其中對象時,
可以使用 Shape
類型來取值, 因爲雖然我們不知道列表中的元素類型具體是什麼類型, 但是我們肯定的是它一定是Shape類的子類型。 所以可以用Shape接收取出的對象,
但不能向其中添加對象。
List<Shape> shapes = new ArrayList<Shape>();
List<? super Cicle> cicleSupers = shapes;
cicleSupers.add(new Cicle()); //OK, subclass of Cicle also OK
cicleSupers.add(new Shape()); //ERROR
這表示cicleSupers 列表存儲的元素爲 Circle
的超類, 因此我們可以往其中加入 Circle對象或者Circle 的子類對象, 但是不能加入 Shape 對象。 這裏的原因在於列表 cicleSupers存儲的元素類型爲Cicle 的超類, 但是具體是 Cicle 的什麼超類並不清楚。 但是我們可以確定的是只要是 Cicle 或者 Circle 的子類, 則一定是與該元素類別兼容。4、泛型方法
泛型方法的格式, 類型參數<T>需要放在函數返回值之前。 然後在參數和返回值中就可以使用泛型參數了。
public static <T> void fromArrayToCollection(T[] a, Collection<T>c){
for(T o : a) {
c.add(o);// correct
}
}
調用方法如下:Object[] oa = new Object[100];
Collection<Object>co = new ArrayList<Object>();
fromArrayToCollection(oa, co);// T inferred to be Object
我們調用方法時並不需要傳遞類型參數,系統會自動判斷類型參數並調用合適的方法。 當然在某些情況下需要指定傳遞類型參數, 比如當存在與泛型方法相同的方法的時候( 方法參數
類型不一致) , 如下面的一個例子:
public <T> void go(T t) {
System.out.println("generic function");
}
public void go(String str) {
System.out.println("normal function");
}
public static void main(String[] args) {
FuncGenric fg = new FuncGenric();
fg.go("haha");//打印 normal function
fg.<String>go("haha");//打印 generic function
fg.go(new Object());//打印 generic function
fg.<Object>go(new Object());//打印 generic function
}
5、方法重載
在 JAVA 裏面方法重載是不能通過返回值類型來區分的, 比如代碼一中一個類中定義兩個如
下的方法是不容許的。 但是當參數爲泛型類型時, 卻是可以的。 如下面代碼所示, 雖然形參
經過類型擦除後都爲 List 類型, 但是返回類型不同, 這是可以的。
/*代碼: 正確 */
public class Erasure{
public void test(List<String> ls){
System.out.println("Sting");
}
public int test(List<Integer> li){
System.out.println("Integer");
}
}
6、泛型數組
不能創建一個確切泛型類型的數組。 如下面代碼會出錯。
List<String>[] lsa = new ArrayList<String>[10]; //compile error.
因爲如果可以這樣, 那麼考慮如下代碼, 會導致運行時錯誤。
List<String>[] lsa = new ArrayList<String>[10]; // 實際上並不允許這樣創建數組
Object o = lsa;
Object[] oa = (Object[]) o;
List<Integer>li = new ArrayList<Integer>();
li.add(new Integer(3));
oa[1] = li;// unsound, but passes run time store check
String s = lsa[1].get(0); //run-time error - ClassCastException
因此只能創建帶通配符的泛型數組, 如下面例子所示, 這回可以通過編譯, 但是在倒數第二行代碼中必須顯式的轉型才行, 即便如此, 最後還是會拋出類型轉換異常,
因爲存儲在 lsa中的是 List<Integer>類型的對象, 而不是 List<String>類型。 最後一行代碼是正確的, 類型匹配, 不會拋出異常。
List<?>[] lsa = new List<?>[10]; // ok, array of unbounded wildcard type
Object o = lsa;
Object[] oa = (Object[]) o;
List<Integer>li = new ArrayList<Integer>();
li.add(new Integer(3));
oa[1] = li; //correct
String s = (String) lsa[1].get(0);// run time error, but cast is explicit
Integer it = (Integer)lsa[1].get(0); // OK
7、不能拋出或捕獲泛型類的實例
public class Problem<T> extends Exception{}//Error
catch 子句中不能使用類型變量, 以下方法不能編譯:
public static <T extends Throwable> void dowork(class<T> t)
{
try {}
catch(T e) //error
{}
}
Public static <T extends Throwable> void doWork(T t) throws T
{
Catch(Throwable realCause)
{
t.initCause(realCause);
throw t;
}
二、使用規範
第23條:不要在新代碼中使用原生態類型
-
在1.5版本以後的代碼建議使用泛型而不是對應的原生態類型,這有助於錯誤的發現,使用Java的特性,避免強制轉換。是用原生態類型失去了泛型在安全性和表述性方面的所有優勢。
-
如果一些情況下客戶代碼不在乎類型參數T到底是什麼的話,也建議使用泛型,如<?>,表示接受任何類型,相比於“T”限制了某一類型,“?”的範圍過得多(?被成爲無限制的通配符)。
第24條:消除非受檢警告
第25條:列表優先於數組
第26條:優先考慮泛型
第27條:優先考慮泛型方法
使用泛型方法可以不用在代碼中顯示進行類型轉換,還可以進行類型推導。
(1).編寫一個集合並集的泛型方法:
public static <E> Set<E> union(Set<E> s1, Set<E> s2){
Set<E> result = new HashSet<E>(s1);
result.addAll(s2);
return result;
}
(2).泛型類型推導:
正常聲明泛型集合如下:
Map<String, List<String>>anagrams = new HashMap<String, List<String>>();
通過集合聲明時的泛型參數類型就可以推導出集合實例的泛型參數類型,這個過程叫泛型類型推導,如果支持泛型類型推導,則上面代碼的HashMap就可以不再指定泛型參數類型,但是目前JDK還沒有內置泛型類型推導,我們可以自己進行一個小的模擬實現:
public static <K, V> HashMap<K, V> newHashMap(){
return new HashMap<K, V>();
}
Map<String, List<String>> anagrams = newHashMap();
泛型單例工廠
泛型單例工廠模式用於創建不可變但又適合於許多不同類型的對象,由於泛型是通過類型檫除實現的,因此可以給所有必要的類型參數使用單個對象,例子如下:
public interface UnaryFunction<T>{
T apply(T arg);
}
private static UnaryFunction<Object> IDENTITY_FUNCTION =
new UnaryFunction<Object>(){
public Object apply(Object arg){
return arg;
}
}
public static <T> UnaryFunction<T> identityFunction(){
return (UnaryFunction<T>) IDENTITY_FUNCTION;
}
由於泛型是類型檫除的,在運行時對於無狀態的泛型參數類型只需要一個泛型單例即可。
泛型遞歸類型限制
使用泛型可以通過某個包含該類型參數本事的表達式來限制類型參數,如
<T extends Comparable<T>>
讀作“針對可以與自身進行比較的每個類型T”,即互比性。
下面的例子是找出列表中實現了Comparable接口的元素的最大值:
public static <T extends Comparable<T>> T max(List<T> list){
Iterator<T> i = list.iterator();
T result = i.next();
while(i.hasNext()){
T t = i.next();
if(t.compareTo(result) > 0){
result = t;
}
return result;
}
}
第28條:利用有限制通配符來提升API的靈活性
由於泛型參數化類型是不可變的,對於任何類型的Type1和Type2而言,List<Type1>既不是List<Type2>的子類型,也不是它的超類型,由此會產生可以將任何對象放進List<Object>中,卻只能將字符串放在List<String>中的問題,解決此類問題我們需要使用泛型的通配符。
例子如下:
自定義堆棧的API如下:
public class Stack<E> {
public Stack();
public void push(E e);
public E pop();
public boolean isEmpty();
public void pushAll(Iterable<E> src){
for(E e : src){
push(e);
}
}
public void popAll(Collection<E> dst){
while(!isEmpty()){
dst.add(pop());
}
}
}
上述代碼編譯完全沒有問題,但是如果想完美運行還需要使用泛型通配符。
(1).生產者限制通配符extends:
使用如下的測試數據對pushAll方法進行測試:
Stack<Number> numberStack = new Stack<Number>();
Iterable<Integer> integers = …;
numberStack.pushAll(integers);
在運行時pushAll方法會報參數類型不匹配錯誤,解決這個問題可以使用限制通配符類型,將pushAll方法修改如下:public void pushAll(Iterable<? extends E> src){
for(E e : src){
push(e);
}
}
Iterable<? extends E>的意思是集合元素的類型是自身的子類型,即任何E的子類型,在本例子Integer是Number的子類,因此正好符合此意。
(2).消費者限制通配符super:
使用下面的測試數據對popAll方法進行測試:
Stack<Integer> integerStack = new Stack< Integer>();
Iterable<Number> numbers = …;
integerStack.popAll(numbers);
在運行時popAll方法會報參數類型不匹配錯誤,解決這個問題可以使用限制通配符類型,將popAll方法修改如下:public void popAll(Collection<? super E> dst){
while(!isEmpty()){
dst.add(pop());
}
}
Collection<? super E>的意思是集合元素的參數類型是自身的超類型,即任何E的超類,在本例中可以將Integer類型的元素添加到其超類Number的集合中。
上述的兩個通配符可以簡記爲PECS原則,即producer-extends,consumer-super.
(3).無限制通配符?:
對於同時具有生產者和消費者雙重身份的對象來說,無限制通配符?更合適,一個交互集合元素的方法聲明如下:
public static void swap(List<?> list, int i, list j);
一般來說,如果類型參數只在方法聲明中出現一次,就可以使用通配符取代它,如果是無限制的類型參數,就使用無限制通配符?代替。
類型安全的異構容器
第29條:優先考慮類型安全的異構容器
一般情況下,集合容器的只能由固定的類型參數,如一個Set只有一個類型參數表示它的類型,一個Map有兩個類型參數表達鍵和值的類型,但是有些情況下我們需要更多的靈活性,即將容器的鍵進行參數化而不是將容器參數化,然後將參數化的鍵提交給容器,來插入或者獲取值,用泛型系統來確保值的類型與它的鍵類型相符。
在JDK1.5之後Class被泛化了,類的類型從字面上來看不再只是簡單的Class,而是Class<T>,例如String.class屬於Class<String>類型,Integer.class屬於Class<Integer>類型。
public class Favorites{
private Map<Class<?>, Object> favorites = new HashMap<Class<?>, Object>();
public <T> void putFavorites(Class<T> type, T instance){
if(type == null){
throws new NullPointerException(“Type is null”);
}
favorites.put(type, type.cast(instance));
}
public <T> T getFavorite(Class<T> type){
return type.cast(favorites.get(type));
}
public static void main(String[] args){
Favorites f = new Favorites();
putFavorite(String.class, “java”);
putFavorite(Integer.class, 0xcafebabe);
putFavorite(Class.class, Favorite.class);
String favoriteString = f.getFavorite(String.class);
Int favoriteInteger = f.getFavorite(Integer.class);
Class<?> favoritesClass = f.getFavorite(Class.class);
System.out.printf(“%s %x %s%n”, favoriteString, favoriteInteger, favoritesClass);
}
}
程序正常打印出 Java cafebabe
Favorites。