1. 泛型類
class Gen<T> { private T t; public T get(){ return t; } public void set(T argt){ t = argt; } }
“<>”內的T為類型參數,只能是類名,不能是基本類型(如int , double),泛型類(以及后面講到的泛型方法)可以有多個類型參數。
class Pair<K,V>{ private K k; private V v; …… }
類型參數可以看做這個泛型類操作的數據類型
泛類型的使用
Gen<String> gs = new Gen<String>(); gs.set("abc"); String str = gs.get();
2. 泛型方法
class GenFun{ public <T> T Mid(T[] a){ return a[a.length/2]; } }
這是一個普通類,但是具有一個泛型方法,返回T類對象數組的中間位置的元素引用,泛型方法需要在返回值前用“<>”說明類型參數。
(1)如果一個泛型類中有泛型方法,泛型方法的類型參數可以與泛型類的類型參數不同。
(2)若在泛型類中的靜態方法要訪問泛型參數,必須使它變成泛型方法。
3.類型參數的限定
限定上限關鍵字 extends
class GenFun{ public <T extends Comparable<T>> T Max(T[] a){ T max = a[0]; for(T t: a){ if(t.compareTo(max) > 0){ max = t; } } return max; } }
<T extends A> 這里的extends表示類型參數T必須就是A類或者A的子類,或者表示T需要實現A這個接口。上面的例子就表示T必須實現Comparable接口(注意Comparable接口本身又是個泛型接口)。
現在我們來舉一個例子,來說明上限限定符的使用規則,假設E繼承 D, D繼承C, C繼承B, B繼承A
class A{}; class B extends A{}; class C extends B{}; class D extends C{}; class E extends D{}; class F{}; class Gen<T extends C> { private T t; public T get(){ return t; } public void set(T argt){ t = argt; } } public class GenTest { public static void main(String[] args) { //---------------------- Gen<F> gf = new Gen<F>();//編譯出錯 F不是C的子類 Gen<B> ga = new Gen<B>();//編譯出錯 B是C的父類 //---------------------- Gen<C> gc = new Gen<C>();//編譯成功 gc.set(new B());//編譯出錯 B不是C的子類 Gen<D> gd = new Gen<D>();//編譯成功 gd.set(new D());//編譯成功 D d0 = gd.get();//編譯成功 gd.set(new E());//編譯成功,把E的對象當做D對象看待 D d1 = gd.get();//編譯成功,注意返回的類型為D } }
有多個限定條件可以用“&”連接,多個限定條件中只能有一個類,其它的都為接口
限定下限關鍵字 super
class Gen<T super D> {//編譯錯誤 …… }
限定條件可用來單獨限定方法(即泛型方法),也可以用來限定整個類。含有通配符的限定條件用於創建泛型類的引用。
4. 擦除
都是Object
實際上java語言中並沒有泛類型對象,所有對象都是普通對象,也就說泛型僅僅存在於編譯階段。這一點我們可以從Gen<T>的字節碼中得到印證。查看字節碼我使用的是Bytecode visualizer插件,可以從http://marketplace.eclipse.org/下載到。
class Gen<T> { /* compiled from GenTest.java * private T t; Gen() { /* L18 */ 0 aload_0; /* this */ 1 invokespecial 12; /* java.lang.Object() */ 4 return; } public java.lang.Object get() { /* L22 */ 0 aload_0; /* this */ 1 getfield 23; /* .t */ 4 areturn; } public void set(java.lang.Object arg0) { /* L26 */ 0 aload_0; /* this */ 1 aload_1; /* argt */ 2 putfield 23; /* .t */ /* L27 */ 5 return; } }
注意get方法的返回值類型和set方法的參數類型都是Object。可以看出泛型類中,將泛型對象都用Object對象代替。由於get方法應該根據具體的類型參數返回一個對應類型的對象,那么這一點又是如何實現的。不妨先來觀察下面的代碼。
public class GenTest { public static void main(String[] args) { Gen<D> gd = new Gen<D>(); gd.set(new D()); D d0 = gd.get(); } }
代碼的意思很簡單就是實例化一個Gen<D>的對象,並調用它的get和set方法。我們再來看看上述代碼對應的字節碼(省略了不相干的部分)。
public static void main(java.lang.String[] args) { /* L69 */ 0 new 16; 3 dup; 4 invokespecial 18; /* javaleanning.Gen() */ 7 astore_1; /* gd */ /* L71 */ 8 aload_1; /* gd */ 9 new 19; 12 dup; 13 invokespecial 21; /* javaleanning.D() */ 16 invokevirtual 22; /* void set(java.lang.Object d0) */ /* L72 */ 19 aload_1; /* gd */ 20 invokevirtual 26; /* java.lang.Object get() */ 23 checkcast 19; /* javaleanning.D */ 26 astore_2; /* d0 */ /* L106 */ 27 return; }
第20行是調用get方法,注意23行,它是進行強制類型轉換,26行是將結果傳遞給引用d0。也就是說泛型的實現原理就是在需要返回某個泛型對象之前,進行強制類型轉換。在使用泛型類中的方法或泛型方法時,會用實際的類名去替換T,這樣一來編譯器就知道了強制轉換的類型。還有一點要說明,強制轉換的代碼是編譯器在調用get方法時插入的,get方法中並沒有進行強制類型轉換(它也不知道該轉換成什么類型的對象),這樣可以使得泛型類和泛型方法的編譯不依賴於T的具體類型。
class Gen<T> { private T t; public T get(){ return t; } public void set(T argt){ t = argt; } public void set(Object argt){ //編譯出錯 t = argt; } }
上述泛型類中添加一個public Object get() ,則會出現編譯錯誤,原因是在編譯時,set(T argt)會將泛類型擦除,變成set(Object argt),這樣一個類中就存在兩個完全相同的方法。
靜態數據公用
因為擦除效應,類型參數不同的泛型類使用的是同一代碼和靜態數據。。我們在G<T>中添加一個靜態變量n
class Gen<T> { private T t; public static int n = 0; public T get(){ return t; } public void set(T argt){ t = argt; } }
現在我們做一個測試。
Gen<D> gd = new Gen<D>(); gd.n = 100; Gen<E> ge = new Gen<E>(); System.out.println(ge.n);
最后的輸出結果是100。
5. 覆蓋
現在有一個SubGen類繼承了Gen<A>,並想覆蓋Gen<A>的set方法。
class SubGen extends Gen<A>{ public void set(Object o){ //編譯出錯 System.out.println("from SubGen set "); } }
雖然Gen<A>被擦除后的set方法的原型就是set(Object o),但是想覆蓋父類G<A>的set方法,上述寫法會出現編譯錯誤。因為編譯器以為它成功的欺騙了我們,讓我們以為存在泛類型,我們這么寫就是告訴它我們識破了騙局,編譯器必然不高興了,編譯就不能成功(真正的原因后面會介紹)。所以我們必須假裝不知道有擦除這回事,按照泛型的方式來處理。編譯器以為我們會認為Gen<A>的set方法的原型是set(A a),所以我們必須這么寫代碼才能實現子類對(泛型)父類中方法的覆蓋。
class SubGen extends Gen<A>{ public void set(A a){ System.out.println("from SubGen set"); } }
但是現在還有個疑問,明明泛型中的類型參數編譯時都替換成了Object,那么子類中的set(A a)就不能覆蓋父類中的set(Object argt)方法了(因為這兩個方法的參數不同),但是我們運行下面的代碼卻能得到正確的結果(輸出 from SubGen set)。
Gen<A> ga = new SubGen(); ga.set(null);
要解釋這個原因,我們可以查看SubGen的字節碼。
public void set(javaleanning.A arg0) { /* L34 */ 0 getstatic 16; /* java.lang.System.out */ 3 ldc 22; /* "from SubGen" */ 5 invokevirtual 24; /* void println(java.lang.String arg0) */ /* L35 */ 8 return; } /* bridge method generated by the compiler */ public volatile void set(java.lang.Object arg0) { /* L1 */ 0 aload_0; 1 aload_1; 2 checkcast 33; /* javaleanning.A */ 5 invokevirtual 35; /* void set(javaleanning.A arg0) */ 8 return; }
可以看到SubGen中有兩個set方法,public void set(javaleanning.A arg0)是我們自己編寫了,而另一個public volatile void set(java.lang.Object arg0) 是編譯器自動幫我們添加的(注意代碼的注釋bridge method generated by the compiler)。添加的這個方法正好和父類中擦除掉泛型的set方法同名且同參數public volatile void set(java.lang.Object arg0),這就實現了子類對父類方法的覆蓋,而set(java.lang.Object arg0)內部就是僅僅調用了我們寫的set(javaleanning.A arg0)方法。現在我們回頭看看前面我們編譯出錯的代碼,原因就顯而易見了。因為編譯器幫我們編寫了一個set(java.lang.Object arg0)方法,如果我們自己也實現一個set(Object o),那么兩個(另一個由編譯器自動生成)一樣的方法必然會產生沖突,所以會編譯失敗。
現在SubGen中添加set方法想要覆蓋掉父類Gen<A>中的set方法。那么我們可以這么寫(代碼沒有什么具體意義,添加輸出語句,只是為了區別子類中的方法和父類中的方法)。
class SubGen extends Gen<A>{ public void set(A a){ System.out.println("from SubGen set"); } public A get(){ System.out.println("from SubGen get"); return new A(); } }
我們使用下面的代碼測試
Gen<A> ga = new SubGen(); ga.set(null); ga.get();
可以得到正確的結果
from SubGen set
from SubGen get
我們查看字節碼可以發現一個有趣的問題
public javaleanning.A get() { /* L38 */ 0 getstatic 16; /* java.lang.System.out */ 3 ldc 22; /* "from SubGen" */ 5 invokevirtual 24; /* void println(java.lang.String arg0) */ /* L39 */ 8 new 34; 11 dup; 12 invokespecial 36; /* javaleanning.A() */ 15 areturn; } /* bridge method generated by the compiler */ public volatile java.lang.Object get() { /* L1 */ 0 aload_0; 1 invokevirtual 38; /* javaleanning.A get() */ 4 areturn; }
SubGen 中有兩個get方法,它們僅僅返回值不同。一般情況下,程序員這樣寫代碼必然會導致編譯錯誤,但是由於是編譯器自己添加的一個方法(java.lang.Object get()),所以的確能夠編譯成功,並且編譯器能夠通過返回值的不同來區分這兩個方法。
6.泛型中的繼承
可以使用子類實例
我們仍然假設E繼承 D, D繼承C, C繼承B, B繼承A。那么Gen<C> 中的方法可以處理 類C的實例,類D的實例,類E的實例,但不能處理類B的實例和類A的實例。
Gen<C> gc = new Gen<C>(); gc.set(new C()); C c = gc.get(); gc.set(new D()); c = gc.get(); D d = (D) gc.get(); gc.set(new E()); c = gc.get(); d = (D) gc.get(); E e = (E) gc.get(); gc.set(new A()); //編譯錯誤 gc.set(new B()); //編譯錯誤
繼承中的限制
雖然,E繼承 D, D繼承C, C繼承B, B繼承A,但是Gen<A>,Gen<B>,Gen<C> ,Gen<D>, Gen<E> 之間沒有任何繼承繼承關系。因為泛類型的本質就是增強強制轉換的安全性,將本來由程序員進行的強制轉換工作交給編譯器來完成。假設由於B繼承了A使得Gen<B>繼承Gen<A>,這就可能導致運行時的錯誤。
Gen<B> gb = new Gen<B>(); Gen<A> ga = gb; //編譯錯誤 ga.set(new A()); B b = gb.get();
一般來說在Gen<B>放入一個B的實例,但是取出一個A的實例一般沒有什么問題,但是如果允許Gen<A> 和 Gen<B> 存在繼承關系,按照上如代碼,我們就可以讓Gen<A>的實例ga和Gen<B>的實例gb指向同一Gen<B>實例,用ga存入一個A類的實例,利用gb的get方法取出一個B類的實例,這就顯然會引起運行時的錯誤。
泛型中的繼承的繼承情況有很多種,可以繼承一個具體的泛型類(像上述的“覆蓋”章節中 class SubGen extends Gen<A>),還一個繼承一個純粹的泛型類
class SonGen<U, T> extends Gen<T>{ private U u;//只是定義一個變量而已 public void set(T argt){// 覆蓋父類中的方法 System.out.println("I am SonGen"); } }
SonGen<U, T> 繼承了 Gen<T> ,並添加了一個類型類型參數。當然還可以繼承具有泛型方法的類,這里就不舉例了。
7.通配符及類型參數的限定
泛型中對繼承的限制是為了解決類型轉換的安全性問題,但是卻違背了java中多態的原則。為了同時解決泛型中的安全性和多態原則,java引用通配符。我們繼續使用上面的Gen<T>和類A、B、C、D、E作為例子。通配符“ ?”表示任意類型,它的使用有三種形式。
通配符與上限限定符
Gen<? extends B >表示Gen的類型參數是類B或者任何類B的子類。Gen<? extends B >中的兩個方法(set 和 get)實際上被通配符和限定符后變成了如下的形式
set( ? extends B )
? extends B get()
我們現在來看一下它的使用
Gen<? extends B> gec = new Gen<D>(); //編譯成功 gec.set(new C()); //編譯錯誤 gec.set(new D()); //編譯錯誤 gec.set(new E()); //編譯錯誤 B b = gec.get(); //編譯成功
現在對上述代碼進行解釋。第一行能編譯成功,是因為滿足 D 是 B 的子類。
最后一行沒有錯誤,因為編譯器只知道gec是對Gen<B>的子類對象的引用,而具體是哪個子類它並不清楚,但是B子類的對象一定可以轉換成B類型的對象。
而所有調用的set方法的語句都會出現編譯錯誤,因為編譯器只知道gec是對Gen<B>的子類對象的引用,而具體是哪個子類它並不清楚(上述代碼中指向了Gen<D>的對象),所以編譯器gec把它所指的對象僅當做當做Gen<B>來使用,所以編譯器拒絕一切 ? extends B 作為參數,這樣做是保障泛型的安全性。比如有個類F,它也繼承了B類,而F類和D類不存在繼承關系。
Gen<D> gd = new Gen<D>(); Gen<? extends B> gec = gd; gec.set(new F());//如果編譯成功 D d = gd.get();
若gec.set(new F())編譯通過,那么D d = gd.get()必然出現運行錯誤,因為返回的實際上是個F類型的對象,卻被轉換成了D類型。
通配符與下限限定符
Gen<? super D>表示Gen的類型參數是類D或者任何類D的父類。Gen<? super D >中的兩個方法(set 和 get)實際上被通配符和限定符后變成了如下的形式
set( ? super D )
? super D get()
我們現在來看一下它的使用
Gen<? super D> supd = new Gen<B>(); supd.set(new A()); //編譯錯誤 A a = supd.get(); //編譯錯誤supd.set(new C()); //編譯錯誤 C c = supd.get(); //編譯錯誤 supd.set(new D()); D d = supd.get(); //編譯錯誤 d = (D)supd.get(); supd.set(new E()); Object o = supd.get();
在對上述代碼進行解釋。第一行能編譯成功,是因為滿足 B 是 D 的父類。
如果不加強制轉換,所有調用的get方法的語句都會出現編譯錯誤,原因其實和上面類似,編譯器只知道supD是對Gen<D>的父類對象的引用,而具體是哪個父類它並不清楚,由於get的返回值類型是? super D編譯器就沒有辦法確定應該轉換成D類具體的哪種類型。
supd.set(new D())和supd.set(new E())能夠編譯成功的原因是,雖然編譯器只知道supD是對Gen<D>的父類對象的引用,而具體是哪個父類它並不清楚,但是D類的實例和E類的實例一定能夠當做D的父類型的實例來對待。
A是D的父類,但是以A類的對象作為參數的get 和 set 方法都會編譯失敗,這似乎和限定符表示的意思不相符。但是考慮以下的代碼:
Gen<B> gb = new Gen<B>(); Gen<? super D> supC = gb; supC.set(new A()); B b = gb.get();
如果upC.set(new A())能夠編譯成功,那么B b = gb.get()一定會出現異常,因為出現了向上轉型(由父類A轉向了子類B)。
無限定通配符
Gen<?> 等價於Gen<? extends Object> 表示Gen的類型參數是Object或者Object的子類
通配符小節
(1) extends 可用於的返回類型限定,不能用於參數類型限定。
(2) super 可用於參數類型限定,不能用於返回類型限定。
(3) 通配符一般用於創建泛型類的引用
假設X表示一個具體的類,注意區別 ? extends X 和Gen<? extends X> 。Gen<? super X>和 Gen<? extends X>兩者可以作為參數類型也可以作為返回值類型,可參見 ArrayList代碼。
public static <T> void copy(List<? super T> dest, List<? extends T> src) { int srcSize = src.size(); if (srcSize > dest.size()) throw new IndexOutOfBoundsException("Source does not fit in dest"); if (srcSize < COPY_THRESHOLD || (src instanceof RandomAccess && dest instanceof RandomAccess)) { for (int i=0; i<srcSize; i++) dest.set(i, src.get(i)); } else { ListIterator<? super T> di=dest.listIterator(); ListIterator<? extends T> si=src.listIterator(); for (int i=0; i<srcSize; i++) { di.next(); di.set(si.next()); } } }
8. 泛型的局限性
雖然java沒有真正的泛型,但是編譯器在編譯階段會做一些語法上的限制
(1)判斷某個對象是否是泛型類的實例的正確方法
Gen<B> gb = new Gen<B>(); if(gb instanceof Gen<?>){ System.out.println("only correct way"); }
(2) class實際上是個泛型類 原型為class<T>。A.class 是 Class<A>的唯一一個實例,String.class是Class<Stiring>的唯一一個實例
(3)不能實例化泛型變量,即不能使用
new T()
T.class
如果要實例化一個泛型變量,可以在泛型類中添加一個靜態方法,並傳入class<T>類型的參數
public static <T> T makeTobj(Class<T> cl){ try { return cl.newInstance(); } catch (InstantiationException | IllegalAccessException e) { } return null; }
(4) 沒有泛型數組,如果需要在泛型類中創建一個泛型數組,可以用Object類型的數組代替。我們可以參照ArrayList的源代碼。
public class ArrayList<E> extends AbstractList<E>implements List<E>, RandomAccess, Cloneable, java.io.Serializable{ //省略無關代碼 transient Object[] elementData;// non-private to simplify nested class access public E get(int index) { rangeCheck(index); return elementData(index); } }
並可以直接返回數組中的某個對象,因為編譯器會在代用該代碼時自動添加強制類型轉換。
如果需要返回泛型的數組,我們可以new Object[] 或者利用Array.newInstance類中的方法創建特定類型的數組,然后進行強制類型轉換(T[])。參見ArrayList代碼。
public static <T,U> T[] copyOf(U[] original, int newLength,Class<? extends T[]> newType){ @SuppressWarnings("unchecked") T[] copy = ((Object)newType == (Object)Object[].class) ? (T[]) new Object[newLength] : (T[]) Array.newInstance(newType.getComponentType(),newLength); System.arraycopy(original, 0, copy, 0, Math.min(original.length, newLength)); return copy; } public <T> T[] toArray(T[] a) { if (a.length < size) // Make a new array of a's runtime type, but my contents: return (T[]) Arrays.copyOf(elementData, size, a.getClass()); System.arraycopy(elementData, 0, a, 0, size); if (a.length > size) a[size] = null; return a; }
雖然不能創建泛型數組,但是可以創建泛型數組的引用 Gen<T>[] genArr;
(5) 沒有泛類型的靜態數據成員