JAVA泛型淺析

Sky-Tiger發表於2007-02-14
本文主要列舉了在使用Java泛型時應該注意的問題。Java泛型是Java5的一個重要特性,它和自動裝箱、變長引數等新特性一起,提升了Java程式碼的健壯性和易用性,但SUN本身過分強調向前的相容性,也引入了不少問題和麻煩。[@more@]

JAVA泛型和C++泛型的區別:

Java的泛型被定義成擦除,而C++的泛型則是擴充套件;

對於C++模板,當引數型別為不同的型別時,生成的模板例項也是不同的型別,如:定義類别範本

Template class A : public B;

當例項化模板時

A a;

A<:string> b;

這裡的ab是兩種不同的型別的例項;

Java則不是這樣的,如泛化List,分別用IntegerString來例項化,

List l

List s

透過反射機制看lsclass,他們都是List!所有的引數型別都被編譯器擦除了!

這樣造成的結果是以往用C++模板可以實現某種契約(contract )的功能在Java中變得很另類了,舉一個例子:

C++程式碼:

Template class A{

Private:

T x;

Public:

Void func(){

int ret = x.foo(12);

}

}

上面這段程式碼在C++中經常能夠看到,它暗中就設定了一個契約:凡是能否例項化這個模板的型別T,其必須具有一個公共的函式foo,這個函式返回整數,並且接受一個整數做為引數。

Java的程式碼:

public class A{

private E e;

public void func(){

int ret = e.foo(12); //編譯錯誤啊

}

}

編譯器給出的錯誤原因是foo函式對於型別中E是未定義的!!造成這樣的問題的原因有兩個:

1、 C++的編譯器直到模板被使用(例項化)的時候才去編譯模板,如果你不去使用模板C++編譯器不會編譯模板;而例項化的時候編譯器已經能夠確定具體的引數型別,所以能夠檢測契約是否符合;Java的編譯器不是這樣工作的,所以它在編譯模板型別的時候不能夠確定E到底有沒有這個foo函式;

2、 型別擦除的結果;修改一下上面的程式,我們看看在func中到底能夠呼叫什麼函式,一看只能呼叫Object物件中的函式。所有的型別E都被擦除成Object了!

如果真的要想實現類似C++的契約,就必須確保引數型別E不被擦除成Object!需要如下修改程式碼:

public class A{

private E e;

public void func(){

int ret = e.foo(12);

}

}

Class B{

Public int foo(int param){…};

}

這樣雖然可以實現我們期望的形式,但是約束的程度要比C++的強很多,C++中,只要任意型別,其具有一個符合契約的函式,就可以例項化模板,而Java中,則要求所有的型別必須是給定型別的子類才可以例項化模板;

擦除的原則:

1、 所有引數化容器類都被擦除成非引數化的(raw type);如ListList>都被擦除成List

2、 所有引數化陣列都被擦除成非引數化的陣列;如List[],被擦除成List[]

3、 Raw type的容器類,被擦除成其自身,如List 被擦除成List

4、 原生型別(int,String還有wrapper類)都擦除成他們的自身;

5、 引數型別E,被擦除成Object

6、 所有約束引數如 Extends E>都被擦除成E

7、 如果有多個約束,擦除成第一個,如,則擦除成Object

例如:

泛化程式碼:

                 List words = new ArrayList();
                               words.add("Hello ");
                               words.add("world!");
                               String s = words.get(0)+words.get(1);
         擦除後就變成了:
                 List words = new ArrayList();
                               words.add("Hello ");
                               words.add("world!");
                               String s = ((String)words.get(0))+((String)words.get(1))
         擦除後的程式碼和以前沒有泛型時候寫的程式碼沒有任何區別!
     再例如:
         泛化程式碼:

public class textReader<T>{

private T a;

public textReader(T b){

this.a = b;

}

public T getA(){

return a;

}

public static void main(String[] agrvs){

String in = "1234567890";

textReader test = new textReader(in);

String out = test.getA();

System.out.println(out);

}

               }
               擦除後(所有型別引數都被去掉,T被擦除成Object)就變成(注意紅色部分)

public class textReader{

private Object a;

public textReader(Object b){

this.a = b;

}

public Object getA(){

return a;

}

public static void main(String[] agrvs){

String in = "1234567890";

textReader test = new textReader (in);

String out = (String)test.getA();

System.out.println(out);

}

               }

擦除所帶來的問題:

1、 靜態成員共享問題

        List ints = Arrays.asList(1,2,3);
               List strings = Arrays.asList("one","two");
               assert ints.getClass() == strings.getClass();

intsstrings兩個物件最終被擦除成具有相同型別的(List)的物件,於是這兩個物件共享List的靜態成員,於是就可以得出這樣的結論,所有泛化型別的靜態成員被其所有的例項化物件共享,因此也就要求所有靜態成員不能夠是泛化的!

class Foo {
  private final T value;
  private static List<T> values = new ArrayList<T>(); //非法
  public T getValue() { return value; }
                   public static List<Object> values =  new ArrayList<Object>() // ok
}

2、 過載(overload)衝突問題

函式過載的定義這樣的:在一個類的範圍內,如果兩個函式具有相同的函式名稱,不同的引數(返回值不考慮)就互相稱為過載函式。看一個例子:

Class A{

Public int foo(int a){};

Public int foo(float f){}; // 是過載,編譯沒有問題

Public int foo(int a){};

Public float foo(int f){}; // 報錯

Public static int foo1(List a){}

Public static int foo1(List s){} //編譯有錯誤,因為所有的List都被擦除成List,這樣兩個函式重複定義,報錯;

Public static int foo1(List a){}

Public static String foo1(List s){} //沒有問題,編譯器不會報錯!

}

3、 介面實現

一個類不能同時實現具有相同擦除效果的介面,例如:

class Foo implements Comparable, Comparable

Comparable, Comparable都被擦除成Comparable

繼承關係

1、 原始繼承:就是我們經常提到的繼承關係,如ArrayListList的子類;

2、 泛化繼承:

a) 泛化後的ArrayList依舊是List的子類;其中T是引數化型別

b) 如果型別T是型別B的子類,那麼List不是List的子類

c) ListList extends T>的子類

d) ListList extends B>的子類

e) ListList super T>的子類

f) ListList super T>的子類

g) 如果型別T是型別B的子類,那麼T[]B[]的子類

3、 關於協變式(covariant)、不變式(invariant)和反變式(contravariant)

a) 陣列和擴充套件類(extends)的泛化是協變式,即如果型別T是型別B的子類,那麼T[]B[]的子類;ListList extends B>的子類

b) 非擴充套件類泛型是不變式,即如果型別T是型別B的子類,那麼List不是List的子類

c) Super類泛型是反變式,即如果BT的超類,則List List super T>的子類

GetPut原則:

當從一個泛化的結構中取資料的時候請使用extends通配,當往一個泛化的結構中放資料的時候請使用super通配;

當需要同時從一個泛化結構中讀取和寫入資料是,請不使用萬用字元號;

為什麼會這樣,我們簡單的分析一下:假定型別T繼承自AB,型別CD又從T型別繼承,那麼List extends T>中存放的只能是T型別或是C或是D型別,裡面存放的型別都可以向上castT,所以從List extends T>中取東西編譯器能夠正確處理,只要對映到T就可以了(T是他們的父類)!往List extends T>放東西就不一樣的,原來裡面放的是T/C還是D都被擦除成T,所以編譯器不知道原來到底存放的是什麼型別,無法保證型別安全,所以這個操作被禁止!

List super T>中存放的是A/B或是T,往List super T>T是允許的,因為T總是可以向上轉換成A或是B,但是從裡面取東西就有問題了,編譯器還是不能夠確定List裡面放的是什麼型別,可能是A也可能是B

具體化:

當一個型別能夠在執行時態被完整的表現,我們就稱為其是可以具體化的,舉一個例子:

List就不是一個可以具體化的型別,因為它在執行時態被擦除成List!所以當處理一些需要執行時檢測型別的操作的時候(如instanceof)就要特別注意。

究竟哪些型別是可具體化的呢;

1)、原始型別,如int

2)、非引數化的類和介面;

3)、非限定的引數化型別,如List>, Map,?>

4)、Raw型別,如 List,ArrayList

5)、所有由可具體化型別組成的陣列,如Number[]List>[]

哪些是不可具體化的型別呢

1)、型別變數,如T;

2),引數化型別,如List

3),有限定的引數化型別,如List extends Number>;

具體哪些操作需要注意區分是否是具體化型別:

來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/71047/viewspace-899480/,如需轉載,請註明出處,否則將追究法律責任。

相關文章