對Java Generic相關知識的總結

 
對於如 List<E> 、 List< String > 、 List ,其中 List<E> 稱爲 parameterized type , E 稱爲 (formal) type parameter , String 稱爲 actual type argument , List 稱爲 raw type 。
Generic 的邏輯意義
原有 java 的類型系統
Generic 爲 java 5 帶來了新的類型,這使得 java 中的類型關係變得更加複雜,要弄清楚加入了 generic 後的類型關係就需要先弄清楚原先 java 中的類型系統。
首先,在類型的定義上,類之間不允許多重繼承,類可以實現多個接口。
其次,在類型的使用上,每個變量都必須有明確的類型,變量只能指向相應類型(或相應類型的子類型)的對象,爲了實現這一規則, compile time 會對所有的賦值操作做檢測,而 runtime 則對所有的顯示類型轉換做檢測。
最後,數組作爲 java 中一個特殊的類型,如果 B extends A ,那麼 B[] extends A[] ,當對數組 element 賦值時, runtime 會做檢測,類型不符則拋 ArrayStoreException ,如下。由於有多重數組的出現,意味着 java 的類型系統種有無限種類型。
// B extends A
A a = new A();
A[] array = new B[1];
Array[0] = a; // ArrayStoreException
我認爲理想狀態下的 generic
首先,假設有 B<T> extends A ,那麼 B<Object> extends A 、 B<String> extends B<Object> ,並且 runtime 對使用到 parameter type 的輸入參數做類型檢測。這跟原先 java 類型系統中的 array 是一致的。與數組相同的還有,因爲有如 B<B<String>> 、 B< B<B<String>>> 等等類型的存在, generic 也可以無限增加可用類型。
其次,當 generic 跟繼承連用時,(在不考慮接口的情況下)有三種新的形式: B<T> extends A 、 B extends A<String> 、 B<T> extends A<T> ,其中第三種情況意味着有 B<String> extend A<String> 。
現實中的 generic
       事實上,在 java 5 中,對於 B<T> extends A , B<Object> 跟 B<String> 之間並不存在繼承關係( invariant subtyping ),這跟數組( covariant subtyping )不同。之所以使用這種做法,我想有以下原因:
首先, java 5 compiler 使用 erasure 來支持 generic ,所有與 generic 相關的信息都不存在於 runtime (見下文中“ generic 的實現”),這就意味着 runtime 無法做如下的類型檢測,而即便 runtime 有條件做類型檢測,也勢必影響代碼的執行效率。
ArrayList<String> strList = new ArrayList<String>();
ArrayList<Object> objList = strList;
objList.add(new Object()); // runtime could not throw exception
其次,考慮下面的例子, B<T> extends A<T> ,有 B<String> extends A<String> ,如果使用 covariant subtyping ,又有 B<String> extends B<Object> ,這意味着存在多重繼承,而多重繼承在 java 裏面是不被允許的。值得注意的是,儘管數組使用 covariant subtyping ,但卻不會導致多重繼承,因爲數組屬於系統類型, java 並不允許數組被繼承。
採用了 invariant subtyping 之後,假如有 A<T> ,由於 A<Object> 不再是其他類型 A<String> 、 A<Integer> 等類型的父類, 則無法聲明可以指向所有 A<T> 類型對象的變量。爲了解決這一問題, java 1.5 引入了 wildcard ,聲明爲 A<?> 類型的變量可以指向所有 A<T> 類型的對象。需要注意的是, wildcard 跟繼承是兩種不同的關係,繼承使類型間呈現樹狀的關係,類型爲 B 的變量可以指向的對象類型必須在以 B 爲根節點的子樹中,而類型爲 A<?> 的變量可以指向的對象類型必須爲類型樹中 A<Object> 或與 A<Object> 平行的節點。最後, wildcard 跟繼承結合使得 A<?> 類型變量能夠指向的對象類型必須在以 A<Object> 及 A<Object> 平行的節點爲根的所有子樹中。
// A<T> extends Object, B extends Object, C extends B, D extends B
A<?> a; // instances of A<Object>, A<String>, A<Integer> can be assigned to this variable
B b; // instance of B, C, D can be assigned to this variable
Generic 的實現
加入了 generic 後 java 的 type safe
       保證 type safe ,其實就關鍵在於確保所有變量所指向的對象的類型必須是正確的。我認爲在理想狀態下,應該實現以下幾點:首先,類型爲 A 的變量所能指向的對象類型必須在以 A 爲根節點的子樹中;其次,類型爲 wildcard 的變量,如 A<?> ,所能指向的對象類型必須在以 A<Object> 及 A<Object> 平行的節點爲根的所有子樹中;最後,所有的顯式轉換在 runtime 必須做類型判定。其中,前兩點由 compiler 實現,最後一點由 jvm 實現,然而事實上, java 5 僅實現了前兩點,而決定不在 runtime 做檢測。
       Compile time 下 generic 的 type safe 主要包括 generic class 跟 generic method 的 type safe ,以下分開討論。
Generic class 的 type safe
       假設有以下的類:
public class A {};
public class B<T> extends A {
public T obj;
}
public class C<T> extends B<T> {
public void set(T obj) { this. obj = obj; }
public T get() { return obj; }
}
對於類型爲 C<String> 的對象,能夠指向它的變量的類型有: A 、 B<String> 、 C<String> 、 B<?> 、 C<?> 。對於類型爲 A 的變量,通過該變量無法訪問到任何與 T 相關的方法或對象變量,很顯然在原有 java 的 type safe 機制仍然有效;對於類型爲 B<String> 、 C<String> 的變量, compiler 對所有通過該變量所訪問的方法( set 、 get )或對象變量 (obj) 進行檢測,所有涉及到 T 的賦值都必須滿足 T=String ,則 type safe 得以保證。對於類型爲 B<?> 、 C<?> 的變量,通過該變量所訪問的方法或對象變量,所有的輸出值中 T 類型被替換成 T 的 bound (見下文中“ type parameter 的限制”),所有輸入值中由於 T 類型未知,所以不能接受任何變量賦值( null 除外)。在理想狀態下,輸入值中 T 類型應該也被替換成 T 的 bound ,然後由 runtime 去做類型判定,但是由於 runtime 沒有 generic 相關的任何信息
C<String> strC = new C<String>();
C<?> c = strC;
// even if the following code pass compile time check, runtime could not throw exception
c.obj = new Object();
c.set(new Object());
// here’s a unexpected exception
String str = strC.obj;
str = strC.get();
在 generic class 的所有方法中, T 的類型被認爲是其 bound 或者 bound 的某個子類。也就是說,首先, T 的變量只能指向類型爲 T 或 T 的子類的對象;其次,通過 T 的變量只能訪問到其 bound 的方法和對象變量。假設以下代碼存在於 C 的 set 方法中:
public void set(T obj;) {
Object temp;
temp = obj; // ok
obj = temp; // ompile error
obj.toString(); // can access Object’s methods
}
Generic method 的 type safe
與 Generic class 不同的是,在 generic method 中, actual type argument 並非指定的,而是由 compiler 推斷出的( Inference )。 Compiler 通過對 generic method 中的輸入變量的類型推斷 type parameter 的類型,如果不能夠得到一個 unique smallest type ,則被視爲 compile error ,參考以下代碼:
public <A> void doublet(A a, A b) {};
// compile error, because String and Integer have both Comparable and Serializable as common supertypes
doublet(“abc”, 123);
當 wildcard 跟 generic method 同時使用時,有以下的特例:
public <T> List<T> test(List<T> list) { return list; }
List<?> wildcardList = new ArrayList<String>();
wildcardList = test(wildcardList);
最後, generic method 中對 type parameter 的使用所必須遵循的規則跟上面所提到的 generic class 的方法中的規則是一樣的。
Erasure 的實現方式
Java 5 在 compiler 中採用 erasure 來實現 generic ,經過 erasure 的處理,所有與 generic 相關的信息將被抹掉( erase ),同時在適當的位置插入顯式類型轉換,最終形成的 byte code 跟 java1.4 的 byte code 沒有什麼不一樣。
首先, parameterized type ,被還原成其 non-parameterized type ,如 List<String> 將變成 List 。
其次, type parameter 被替換成它的 bound ,如 T 將變成 Object (假如它的 upper bound 是 Object )。
接着,對於方法類成員的返回值,如果其類型爲 parameter type , erasure 則會插入顯式轉換。如:
public class A<T> {
public T get() { return null; }
}
A<String> a = new A<String>();
String temp = a.get();
// translate to
public class A {
public Object get() { return null; }
}
A a = new A();
String temp = (String) a.get();
最後 erasure 將在必要的時候插入 bridge method 。對於以下的代碼
public class A<T> {
private T obj;
public void set(T obj) { this.obj = obj; }
public T get() { return obj; }
}
public class B extends A<String> {
public void set(String obj) {};
public String get() { return null;}
}
A<String> a = new B();
a.set(“abc”);
String temp = a.get();
在沒有 bridge method 存在的情況下,對於 a 的方法的調用將無法獲得多態性的支持,原因是 B 中的方法的 signature 跟 A 的不同,所以不被 jvm 視爲重載。這時候 erasure 必須在 B 中插入如下的 bridge method :
public void set(Object obj) { set((String) obj);}
public Object get() { return get(); }
需要注意的是 get 的 bridge method 在是編譯不過的,因爲 java 不允許這種形式的 overload ,事實上, bridge method 是直接在 byte code 中插入的。
最後值得注意的是, bridge method 只有在需要的時候被插入,如果 B 不重載 get 跟 set 方法,將不會有 bridge method 存在。
由於 runtime 缺乏 generic 相關的信息而導致的各種限制
1.       通過 wildcard 類型的變量訪問方法及對象變量受到限制(如上文所述)。
2.       與 type parameter 相關的顯式轉化無法保證 type safe ,同時 compiler 會有 warning 。
List<?> list = new ArrayList<String>();
List<String> strList = (List<String>) list; // warning
public <T> T test1(Object obj) { return (T) obj; } // warning
public <T> T[] test2(Object[] objs) { return (T[]) objs; } // warning
3.       在創建某些類型對象時受到限制。
public <T> T test1(T sample) { return new T(); } // compile error
public <T> T[] test2(T sample) { return new T[0]; } // compile error
值得注意的是,即便提供了 actual type argument ,依然無法創建 parameterized type 的數組:
// compile error, but assumes that compiler allow to create such kind of array
List<String>[] lists = new List<String>[1];
List<Integer> intList = new List<Integer>();
intList.add(1);
Object[] objs = lists;
objs[0] = intList; // runtime could not throw an ArrayStoreException for this
String temp = lists[0].get(0); // unexpected error
通過 Class<T> 能夠創建 T 的對象, Class<T> 的奧妙在於,一方面它能夠通過 compiler 的檢測,另一方面,它本身攜帶的信息也足以讓 runtime 得以創建 T 的對象。
public T create(Class<T> c) { return c.newInstance(); };
String temp = create(String.class);
4.       不得不插入 bridge method (如上文所述)。
5.       使用 instanceof 時受到限制。
List<String> list = new ArrayList<String>();
boolean temp;
temp = list instanceof List<String>; // compile error
temp = list instanceof List<?>; // ok
6.       使用 reflection 時存在安全隱患。
public class A<T> {
public T obj;
}
public class B {
public A<String> a;
}
A<Integer> a = new A<Integer>();
B b = new B();
B.class.getField(“a”).set(a); // everything ok
String temp = b.a.obj; // unexpected error
7.       Generic class 中的 type parameter 在其靜態方法及靜態變量中無法使用。
在 generic 中對 type parameter 的限制
使用 extends 關鍵字
對於如 A<T> 的 generic class ,可以使用 extends 來進一步限制 T 所能代表的類型。如:
public class A<T extends Number> { … }
A<Object> objA; // compile error
A<String> strA; // compile error
A<Number> numA; // ok
A<Integer> intA; // ok
這裏, extends 意味着 T 必須是 Number 或者 Number 的子類,以下是對於 extends 更爲複雜的使用。
public class B<T extends B<T>> {…}
public class C extends B<C> { … }
public class D extends C { … }
C c = new C();
D d = new D();
B<C> bc; // ok
bc = c;
bc = d;
B<D> bd; // compile error
B<Object> b; // compile error
這裏,顯然 B<Object> 是非法的,對於 B<D> ,雖然 D 繼承了 C ,但是把 D 替換到“ T extends B<T> ”中,顯然“ D extends B<D> ”不成立,所以 B<D> 也是非法的,與此類似的是 java.lang 裏面的“ Enum<E extends Enum<E>> ”,這一聲明保證了,假設有類 Test ,它不是 Enum 類型(編譯器保證只有使用 enum 關鍵字創建時才能滿足 Enum 類中對 T 的限制),那麼無法聲明 Enum<Test> 類型的變量。
public class E<T, S extends T> { … }
E<Number, Number> e1; // ok
E<Number, Integer> e2; // ok
E<Number, String> e3; // compile error
這裏, extends 用來限制不同的 type parameter(T 、 S) 之間的關係。
最後,需要注意的是,在 generic class 裏面通過使用 extends 對 type parameter 進行限制所導致的結果是刪除了一部分類型(如上述的 E<Number,String> ),而並非阻止這一類型的對象的創建,而在 generic method 裏面使用 extends 則在於阻止某些類型的對象作爲輸入參數,如:
public <T extends String> void test(List<T> list) {}
test(new ArrayList<String>()); // ok
test(new ArrayList<Object>()); // compile error
關於 wildcard – “?”
在不使用 super 關鍵字的時候, ”?” 可以理解成 parameter type 的匿名形式,事實上,這些 ”?” 都可以轉變成用 parameter type 表示。
public void test(List<? extends String> list) {}
public <T extends String> void test(List<T> list) {}
“?” 只能作爲 parameterized type 的 actual type argument 使用,同時由於 ”?” 是匿名的形式,編譯器並不會認爲出現兩次的 A<?> 要求相同的 type parameter 。
public void test(? obj) {} // compile error
public <T> void test1(List<T> list1, List<T> list2) {}
public void test2(List<?> list1, List<?> list2) {}
test1(new ArrayList<Object>(), new ArrayList<String>()); // compile error
test2(new ArrayList<Object>(), new ArrayList<String>()); // ok
使用 super 關鍵字
對於 T super A , super 表示 T 必須是 A 或者 A 的父類,相比起 extends ,對 super 的使用有更多的限制。考慮以下代碼:
public <T super Number> void test(T obj) {} // assumes that there’s no compile error
test(new Object()); // this looks reasonable
test(1); // Integer is not super class of Number, so compiler should reject this, but Integer is also an object, why would a method accept object as valid argument but not integer?
可以看到, super 限制對象類型必須是某個類或其父類,而繼承則允許父類的變量接受子類的對象,這兩者是互相抵觸的,所以對 super 的時候有以下的限制:
首先, super 只能在 parameterized type 的 actual type argument 中作爲限制條件使用,在這個時候,它並不和繼承相抵觸。如:
public void test(List<? super Number> list) {}
其次, super 不能用於限制非匿名的 parameter type ,顯然如果可以這樣的話,就會出現上述代碼中的錯誤。這就決定了 super 只能與 ”?” 連用。
Generic 的向前兼容
下面以 List<E> 爲例,爲了向前兼容原先的代碼,允許使用 List 這樣的 raw type 。在語義上, List 跟 List<Object> 是一致的,然而在語法上 List 跟 List<?> 更爲類似,並且編譯器允許原先在 List 使用原先在 List<?> 上禁止的某些操作(對於 List<?> 爲 compile error 的操作在 List 上僅僅是 warning )。
首先,在類型轉換上, List 跟 List<?> 是等價的, List 類型可以賦給任何指定了 actual type argument 的 parameterized type (如 List<String> ),而 List<?> 則不行(除非使用顯式轉換)。
List list;
List<?> wildcardList;
List<String> strList;
 
list = wildcardList;
list = strList;
wildcardList = list;
wildcardList = strList;
 
strList = list; // warning
strList = wildcardList; // compile error
其次, compiler 不允許通過 List<?> 訪問任何輸入參數與 E 相關的方法,而 List 則僅給出 wanring 。
list.add(“abc”); // waring
wildcardList.add(“abc”); // compile error
其他
關於 GJ
GJ 是使 java 支持 generic 的一個開源項目, java 5 的 generic 是參照 GJ 實現的, GJ 的語法以及實現方式基本上跟目前 java 5 的 generic 相同,當然在某些細節方面 java 5 的 generic 做了改動,研究 GJ 有利於更好的理解 java 5 的 generic 。
Security Implications
考慮以下的代碼:
public class SecureChannel extends Channel {
public String read();
}
public class A {
public LinkedList<SecureChannel> cs;
}
由於 LinkedList<SecureChannel> 在 runtime 會退化成 LinkedList ,惡意的代碼很容易在 cs 裏面放入其他類型的 Channel 。 GJ 中建議使用 type specialization 來解決這一問題,但是由於 java 5 的 generic 僅在需要的時候插入 bridge 方法,所以這一方法在 java 5 中是無效的。
在方法參數中使用 extends 、 super 關鍵字的技巧
考慮如下代碼:
public class A<T> {
private T obj;
public void set(T obj) { this.obj = obj; }
public T get() { return obj; }
}
public class Test<T> {
public void test(A<T> a) { … }
}
A<Object> a = new A<Object>();
A<Number> numA = new A<Number>();
A<Integer> intA = new A<Integer>();
Test<Number> test = new Test<Number>();
test.test(a); // compile error (1)
test.test(numA); // ok
test.test(intA); // compile error (2)
對於 test 方法,如果方法內僅需要調用 A<T> 的 set 方法(即僅需要用到輸入值爲 T 類型的方法,注意,這不包括如 List<T> 這種類新),使用 A<? super T> 代替 A<T> 可能會更爲合適,這使得 (1) 得以編譯通過,由於僅需要調用 A<T> 的 set 方法, A<Object>.set(Object) 顯然比 A<Number>.set(Number) 允許更多的類型,從而使 A<Object> 可以替換 A<Number> 。相似的,如果 test 方法內僅需要調用 A<T> 個 get 方法,則使用 A<? extends T> 代替 A<T> 可能會更合適,這使得 (2) 得以編譯通過。
關於 type parameter 的命名規範
推薦使用精簡同時有意義的名稱,如 E for element 、 T for type (最好是單個字母),同時避免使用任何小寫字母以使得 type parameter 能夠從一般的類還有接口名稱中被區分出來。如果需要同時使用多個 type parameter ,則考慮使用鄰近的幾個不同字母,如 T 、 S 。如果在某個類中已經使用了某個字母作爲 type parameter ,則在其 generic method 以及 nested class 中避免使用同樣的字母。
Generic method 採用 inference 所產生的問題
public interface I {}
public class A implements I {}
public class B{}
public <T> void test(T a, T b);
test(new A(), new B()); // ok
當代碼改動 B 也需要實現接口 I 的時候:
public class B implements I {}
test(new A(), new B()); // compile error
仍然搞不懂的地方
對於類似 java.util.Collections 的 max 方法,經過我的試驗以下兩種聲明方式所能接受的類型是一樣的,不明白它爲什麼要用前者。
public static <T extends Object & Comparable<? super T>> T max1(Collection<? extends T> coll)
public static <T extends Object & Comparable<? super T>> T max2(Collection<T> coll)
參考資料
GJ- Making the future safe for the past: Adding Genericity to the JavaTM Programming Language
Generics in the Java Programming Language
發佈了9 篇原創文章 · 獲贊 3 · 訪問量 19萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章