Java核心技術第八章——泛型程式設計(1)
1.泛型程式設計
泛型程式設計意味著編寫的程式碼可以被很多不同型別的物件所重用。例如:不希望為了聚集String和Integer物件分別設計不同的類。(個人覺得此處說的聚集譯為:建立一個物件,屬性可以為String和Integer型別。但是卻有著相同的行為或屬性)
程式碼如下:
public class StringTest { private String age; public String getAge() { return age; } public void setAge(String age) { this.age = age; } }
public class IntegerTest { private Integer age; public Integer getAge() { return Age; } public void setAge(Integer age) { this.age= age; } }
可以看到上面兩個類都有size屬性和方法。只是他們的屬性型別有所不同。那麼我們則可以使用到泛型:例
public class GenericTest<T> { private T size; public T getSize() { return size; } public void setSize(T size) { this.size = size; } }
然後只需要如下呼叫
public static void main(String[] args){ GenericTest<String> genericString= new GenericTest<>(); GenericTest<Integer> genericInteger = new GenericTest<>(); }
這樣我們就很好的解決了程式碼重用的問題,不是嗎?看不懂?不急,我只是說了泛型的好處,讓我們慢慢往下了解。
2.定義簡單的泛型類
沒接觸過泛型的同學看到上面例子中的T可能會覺得一臉懵逼,其實T 只是一個類的型別變數(雖然你可以定義A、B、C...,但是我們還是要規範點的是吧),然後T需要使用尖括號括起來,當你定義多個型別變數時。可以看下面的Generic類,
public class Generic<T,U> { private T size; private U length; ... }
當你傳入Generic<Integer,String> 時,你可以把這個類想象成如下使用
public class Generic { private Integer size; private String length; ... }
所以,泛型類其實可以看做普通的工廠類。
3.泛型方法
上面介紹了一個簡單的泛型類,讓我們來看看泛型方法是怎麼寫吧。
public class ArrayAlg { public static <T> T getMiddle(T... a){ return a[a.length/2]; } }
可以看到,ArrayAlg是一個普通類,並不是一個泛型類。說明:泛型方法可以定義在普通類和泛型類中。
當我們呼叫getMiddle方法時,在方法名前的尖括號放入具體型別:
String s = ArrayAlg.<String>getMiddle("John","Q","Min");
看起來是不是很彆扭?其實可以省略<String>型別引數,編譯器會使用String[ ] 陣列與泛型T[ ] 進行匹配推斷出一定是String,這個應該就是他們私下bilibili的規則了。變成:
String s = ArrayAlg.getMiddle("John","Q","Min");
有的小夥伴可能會想到,醬紫的話,我能不能瞎比的傳引數進去?例:
Double middle2 = ArrayAlg.getMiddle(3.14,0);
其實當你這樣寫的時候編譯器就已經提示錯誤了,因為第二個引數編譯器會預設認為是Integer型別。解決方法有兩種:
1.把Double改成Number型別,因為Number型別為Double和Integer的父類。
2.自己改程式碼去吧,誰讓你瞎搞。
4.型別變數的限定
有時候,類或方法需要對型別變數加以約束 ,假如,某個泛型方法需要使用到Comparable介面的compareTo方法,那我們則需要限制傳進來的引數必須實現Comparable介面,例:
public static <T extends Comparable> T minmax(T[] a){ compareTo... }
傳進來的引數a則必須實現Comparable介面。假如傳進來引數需要實現Comparable和Serializable兩個介面,則
public static <T extends Comparable&Serializable> Pair<T> minmax(T[] a){ compareTo... }
叮咚!那麼傳進來的引數需要繼承某個類怎麼寫?其實是一樣的。傳進來的引數需要繼承某個類、實現某個介面都是使用extends關鍵字來控制。原因:選擇關鍵字extends的原因是更接近子類的概念,並且Java的設計者也不打算在語言中再新增一個新的關鍵字
5.泛型程式碼和虛擬機器
5.1型別擦除
Java 的泛型在編譯器有效,在執行期被刪除,也就是說所有泛型引數型別在編譯後都會被清除掉
例如:
public class GenericClass<T> { private T first; public T getFirst() { return first; } ... }
擦除型別後
public class GenericClass { private Object first; public Object getFirst() { return first; } ... }
可以看到,型別擦除後的GengericClass和沒引用泛型沒有什麼兩樣。而且,當你使用GenericClass<String>後,也是變成原始的GenericClass型別。
但是當我們使用限定型別變數後,型別擦除後將會使用限定型別。例:
public class GenericClass<T extends Comparable> { private T first; ... }
可以看到限定了傳進來的引數需要實現Comparable介面,那麼型別擦除後如下:
public class GenericClass{ private Comparable first; ... }
問題:當你限定了引數需要實現Comparable和Serializable介面,就是GenericClass<T extends Comparable&Serializable> 時,型別擦除後會變成怎樣呢?
public class GenericClass{ private Comparable first; ... }
虛擬機器會把首位實現的介面(此處為Comparable),轉為擦除後物件型別。當然,如果你這樣寫GenericClass<T extends Serializable&Comparable> ,那麼擦除後的型別為Serializable。
5.2翻譯泛型表示式
當程式呼叫泛型方法時,如果擦除返回型別, 編譯器將插入強制型別轉換。例如List集合原始碼:
public interface List<E> extends Collection<E> { ... E get(int index); ... }
當不使用型別變數時,取出來的值是Object,而定義了String型別變數時,編譯器將插入強制型別轉換:
List list1 = new ArrayList(); list1.add("1"); list1.get(0); //Object List<String> list2 = new ArrayList<>(); list2.add("1"); list2.get(0); //String
但是在虛擬機器中時,list2.get(0)在呼叫時翻譯成了兩條虛擬機器命令:
1.對原始方法List集合的get方法呼叫
2.將返回的Object進行強制型別轉換成String
5.3翻譯泛型方法
當GenericSubClass類繼承了Generic類時,泛型方法會有什麼變化?
public class Generic<T> { private T size; public T getSize() { return size; } ... }
public class GenericSubclass extends Generic<Integer> { @Override public Integer getSize() { return super.getSize(); } }
此時的@Override真的是重寫父類方法嗎?到了執行期間,Generic被型別擦除後getSize方法返回型別變成Object:
public Object getSize() { return size; }
而GenericSubclass的getSize方法的返回型別卻是Integer:
public Integer getSize() { return super.getSize(); }
這並不是重寫父類的方法,因為方法並不一樣,所以導致GenericSubclass在虛擬機器中卻有了兩個getSize方法:
public Integer getSize() {...} public Object getSize() {...}
導致的原因就是型別擦除與多型發生了衝突。要解決此問題,虛擬機器自動在GenericSubclass中生成了一個橋方法:
public Integer getSize() { return (Integer)super.getSize(); }
最後,虛擬機器就會呼叫自己生成的橋方法來解決此衝突。
若有不足之處,請各位大牛在下面留言指出,Thanks♪(・ω・)ノ