編寫高質量程式碼:改善Java程式的151個建議(第7章:泛型和反射___建議93~97)
我們最大的弱點在於放棄。成功的必然之路就是不斷的重來一次。 --達爾文
建議93:Java的泛型是可以擦除的
建議94:不能初始化泛型引數和陣列
建議95:強制宣告泛型的實際型別
建議96:不同的場景使用不同的泛型萬用字元
建議97:警惕泛型是不能協變和逆變的
泛型可以減少將至型別轉換,可以規範集合的元素型別,還可以提高程式碼的安全性和可讀性,優先使用泛型。
反射可以“看透”程式的執行情況,可以讓我們在執行期知曉一個類或例項的執行情況,可以動態的載入和呼叫,雖然有一定的效能憂患,但它帶給我們的便利大於其效能缺陷。
建議93:Java的泛型是可以擦除的
1、Java泛型的引入加強了引數型別的安全性,減少了型別的轉換,Java的泛型在編譯器有效,在執行期被刪除,也就是說所有的泛型引數型別在編譯後會被清除掉,我們來看一個例子,程式碼如下:
兩個一樣的方法衝突了?
這就是Java泛型擦除引起的問題:在編譯後所有的泛型型別都會做相應的轉化。轉換規則如下:
- List<String>、List<Integer>、List<T>擦除後的型別為List
- List<String>[] 擦除後的型別為List[].
- List<? extends E> 、List<? super E> 擦除後的型別為List<E>.
- List<T extends Serializable & Cloneable >擦除後的型別為List< Serializable>.
2、明白了這些規則,再看如下程式碼:
public static void main(String[] args) {
List<String> list = new ArrayList<String>();
list.add("abc");
String str = list.get(0);
}
進過編譯後的擦除處理,上面的程式碼和下面的程式時一致的:
public static void main(String[] args) { List list = new ArrayList(); list.add("abc"); String str = (String) list.get(0); }
3、Java之所以如此處理,有兩個原因:
① 避免JVM的執行負擔。
如果JVM把泛型型別延續到執行期,那麼JVM就需要進行大量的重構工作了。
② 版本相容
在編譯期擦除可以更好的支援原生型別(Raw Type),在Java1.5或1.6...平臺上,即使宣告一個List這樣的原生型別也是可以正常編譯通過的,只是會產生警告資訊而已。
4、明白了Java泛型是型別擦除的,我們就可以解釋類似如下的問題了:
① 泛型的class物件是相同的:每個類都有一個class屬性,泛型化不會改變class屬性的返回值,例如:
public static void main(String[] args) {
List<String> list = new ArrayList<String>();
List<Integer> list2 = new ArrayList<Integer>();
System.out.println(list.getClass());
System.out.println(list.getClass()==list2.getClass());
}
以上程式碼返回true,原因很簡單,List<String>和List<Integer>擦除後的型別都是List,沒有任何區別。
② 泛型陣列初始化時不能宣告泛型,如下程式碼編譯時通不過:
List<String>[] listArray = new List<String>[];
原因很簡單,可以宣告一個帶有泛型引數的陣列,但不能初始化該陣列,因為執行了型別擦除操作,List<Object>[]與List<String>[] 就是同一回事了,編譯器拒絕如此宣告。
③ instanceof不允許存在泛型引數
以下程式碼不能通過編譯,原因一樣,泛型型別被擦除了:
建議94:不能初始化泛型引數和陣列
泛型型別在編譯期被擦除,我們在類初始化時將無法獲得泛型的具體引數,比如這樣的程式碼:
這段程式碼是編譯不過的,因為編譯時需要獲得T型別,但泛型在編譯期型別已經被擦除了。在某些情況下,我們需要泛型陣列,那該如何處理呢?程式碼如下:
public class Student<T> {
// 不再初始化,由建構函式初始化
private T t;
private T[] tArray;
private List<T> list = new ArrayList<T>();
// 建構函式初始化
public Student() {
try {
Class<?> tType = Class.forName("");
t = (T) tType.newInstance();
tArray = (T[]) Array.newInstance(tType, 5);
} catch (Exception e) {
e.printStackTrace();
}
}
}
此時,執行就沒有什麼問題了,剩下的問題就是怎麼在執行期獲得T的型別,也就是tType引數,一般情況下泛型型別是無法獲取的,不過,在客戶端呼叫時多傳輸一個T型別的class就會解決問題。
類的成員變數是在類初始化前初始化的,所以要求在初始化前它必須具有明確的型別,否則就只能宣告,不能初始化。
建議95:強制宣告泛型的實際型別
Arrays工具類有一個方法asList可以把一個邊長引數或陣列轉變為列表,但它有一個缺點:它所生成的list長度是不可變的,而在我們的專案開發中有時會很不方便。如果期望可變,那就需要寫一個數組的工具類了,程式碼如下:
class ArrayUtils {
// 把一個變長引數轉化為列表,並且長度可變
public static <T> List<T> asList(T... t) {
List<T> list = new ArrayList<T>();
Collections.addAll(list, t);
return list;
}
}
這很簡單,與Arrays.asList的呼叫方式相同,我們傳入一個泛型物件,然後返回相應的List,程式碼如下:
public static void main(String[] args) {
// 正常用法
List<String> list1 = ArrayUtils.asList("A", "B");
// 引數為空
List list2 = ArrayUtils.asList();
// 引數為整型和浮點型的混合
List list3 = ArrayUtils.asList(1, 2, 3.1);
}
這裡有三個變數需要說明:
1、變數list1:變數list1是一個常規用法,沒有任何問題,泛型實際引數型別是String,返回結果就是一個容納String元素的List物件。
2、變數list2:變數list2它容納的是什麼元素呢?我們無法從程式碼中推斷出list2列表到底容納的是什麼元素(因為它傳遞的引數是空,編譯器也不知道泛型的實際引數型別是什麼),不過,編譯器會很聰明地推斷出最頂層類Object就是其泛型型別,也就是說list2的完整定義如下:
List<Object> list2 = ArrayUtils.asList();
如此一來,編譯器就不會給出" unchecked "警告了。現在新的問題又出現了:如果期望list2是一個Integer型別的列表,而不是Object列表,因為後續的邏輯會把Integer型別加入到list2中,那該如何處理呢?
強制型別轉換(把asList強制轉換成List<Integer>)?行不通,雖然Java泛型是編譯期擦出的,但是List<Object>和List<Integer>沒有繼承關係,不能強制轉換。
重新宣告一個List<Integer>,然後讀取List<Object>元素,一個一個地向下轉型過去?麻煩,而且效率又低。
最好的解決辦法是強制宣告泛型型別,程式碼如下:
List<Integer> intList = ArrayUtils.<Integer>asList();
就這麼簡單,asList方法要求的是一個泛型引數,那我們就在輸入前定義這是一個Integer型別的引數,當然,輸出也是Integer型別的集合了。
3、變數list3:變數list3有兩種型別的元素:整數型別和浮點型別,那它生成的List泛型化引數應該是什麼呢?是Integer和Float的父類Number?你太高看編譯器了,它不會如此推斷的,當它發現多個元素的實際型別不一致時就會直接確認泛型型別是Object,而不會去追索元素的公共父類是什麼,但是對於list3,我們更期望它的泛型引數是Number,都是數字嘛,參照list2變數,程式碼修改如下:
List<Number> list3 = ArrayUtils.<Number>asList(1, 2, 3.1);
Number是Integer和Float的父類,先把三個輸入引數、輸出引數同類型,問題是我們要在什麼時候明確泛型型別呢?一句話:無法從程式碼中推斷出泛型的情況下,即可強制宣告泛型型別。
建議96:不同的場景使用不同的泛型萬用字元
Java泛型支援萬用字元(Wildcard),可以單獨使用一個“?”表示任意類,也可以使用extends關鍵字表示某一個類(介面)的子型別,還可以使用super關鍵字表示某一個類(介面)的父型別,但問題是什麼時候該用extends,什麼該用super呢?
1、泛型結構只參與 “讀” 操作則限定上界(extends關鍵字),也就是要界定泛型的上界
編譯失敗,失敗的原因是list中的元素型別不確定,也就是編譯器無法推斷出泛型型別到底是什麼,是Integer型別?是Double?還是Byte?這些都符合extends關鍵字的定義,由於無法確定實際的泛型型別,所以編譯器拒絕了此類操作。
2、泛型結構只參與“寫” 操作則限定下界(使用super關鍵字),也就是要界定泛型的下界
甭管它是Integer的123,還是浮點數3.14,都可以加入到list列表中,因為它們都是Number的型別,這就保證了泛型類的可靠性。
建議97:警惕泛型是不能協變和逆變的
協變:窄型別替換寬型別
逆變:寬型別替換窄型別
1、泛型不支援協變,編譯不通過,,,窄型別變成寬型別(Integer>>Number)
泛型不支援協變,但可以使用萬用字元模擬協變,程式碼如下:
" ? extends Number " 表示的意思是,允許Number的所有子類(包括自身) 作為泛型引數型別,但在執行期只能是一個具體型別,或者是Integer型別,或者是Double型別,或者是Number型別,也就是說萬用字元只在編碼期有效,執行期則必須是一個確定的型別。
2、泛型不支援逆變
" ? super Integer " 的意思是可以把所有的Integer父型別(自身、父類或介面) 作為泛型引數,這裡看著就像是把一個Number型別的ArrayList賦值給了Integer型別的List,其外觀類似於使用一個寬型別覆蓋一個窄型別,它模擬了逆變的實現。