泛型初探
在泛型(Generic type或Generics)出現之前,是這麼寫程式碼的:
public static void main(String[] args) { List list = new ArrayList(); list.add("123"); list.add("456"); System.out.println((String)list.get(0)); }
當然這是完全允許的,因為List裡面的內容是Object型別的,自然任何物件型別都可以放入、都可以取出,但是這麼寫會有兩個問題:
1、當一個物件放入集合時,集合不會記住此物件的型別,當再次從集合中取出此物件時,該物件的編譯型別變成了Object
2、執行時需要人為地強制轉換型別到具體目標,實際的程式絕不會這麼簡單,一個不小心就會出現java.lang.ClassCastException,即型別轉換異常
所以,泛型出現之後,上面的程式碼就改成了大家都熟知的寫法:
public static void main(String[] args) { List<String> list = new ArrayList<String>(); list.add("123"); list.add("456"); System.out.println(list.get(0)); }
這就是泛型。泛型是對Java語言型別系統的一種擴充套件,有點類似於C++的模板,可以把型別引數看作是使用引數化型別時指定的型別的一個佔位符。引入泛型,是對Java語言一個較大的功能增強,帶來了很多的好處:
1、型別安全。型別錯誤現在在編譯期間就被捕獲到了,而不是在執行時當作java.lang.ClassCastException展示出來,將型別檢查從執行時挪到編譯時有助於開發者更容易找到錯誤,並提高程式的可靠性
2、消除了程式碼中許多的強制型別轉換,增強了程式碼的可讀性
3、為較大的優化帶來了可能
getClass()相同
看一段程式碼:
public static void main(String[] args) { List<String> stringList = new ArrayList<String>(); List<Integer> integerList = new ArrayList<Integer>(); System.out.println(stringList.getClass() == integerList.getClass()); }
執行結果為:
true
這意味著,泛型是什麼並不會對一個物件例項是什麼型別的造成影響,所以,通過改變泛型的方式試圖定義不同的過載方法也是不可以的:
儘量使用精確的型別定義泛型
儘量使用精確的型別定義泛型,除非必要,否則不要寫一個介面或者父類上去:
public static void main(String[] args) { List<Number> list = new ArrayList<Number>(); list.add(4); list.add(2.2); for (Number number : list) System.out.println(number); }
就像這樣,list中的是一個Number型別,往裡面新增的是Integer與Double,這樣導致get出來的元素也都是Number型別的,失去了子類擴充套件的功能。如果要讓子類變為Interger和Double也可以,(Integer)list.get(0)和(Double)list.get(1)強轉就可以了,但是這樣不就失去了泛型的意義了嗎?所以,儘量用精確的型別去定義泛型。
使用型別萬用字元
List<Object>不是List<String>的父型別,List<Integer>不是List<Number>的父型別,試圖用以下方式賦值是不允許的:
1 public static void main(String[] args) 2 { 3 List<Number> numberList = new ArrayList<Number>(); 4 List<Integer> integerList = new ArrayList<Integer>(); 5 numberList = integerList; 6 }
第5行將報錯"Type mismatch: cannot convert from List<Integer> to List<Number>"。有人可能覺得這樣很不方便:我在一個方法裡面只需要迴圈檢索一個List,也不能利用多型放一個父型別進去,也不能過載,那怎麼辦呢?針對這個問題,Java給開發者提供了萬用字元"?",看一下:
public static void main(String[] args) { List<String> stringList = new ArrayList<String>(); List<Integer> integerList = new ArrayList<Integer>(); printList(stringList); printList(integerList); } private static void printList(List<?> l) { for (Object o : l) System.out.println(o); }
<?>是型別萬用字元,表示是任何泛型的父型別,這樣List<Object>、List<String>這些都可以傳遞進入printList方法中,注意這裡的引數不能寫成List<E>,這樣就報錯了,E未定義。當然<?>也可以不加,不過這樣會有警告:如果傳遞一個List<E>給List,相當於傳遞一個只承諾將它當作List(原始型別)的方法,這將會破壞使用泛型的型別安全。
再注意一點,使用型別萬用字元,只能從中檢索元素,不能新增元素。
泛型方法
public static void main(String[] args) { System.out.println(ifThenElse(false, "111", "222")); } private static <T> T ifThenElse(boolean b, T first, T second) { return b ? first : second; }
返回結果為:
222
這說明,方法也可以被泛型化,不管定義在其中的類是不是泛型化的。這意味著不用顯式告訴編譯器,想要T什麼值:編譯器只知道這些T都必須相同。
靜態資源不認識泛型
接上一個話題,如果把<T>去掉,那麼:
報錯,T未定義。但是如果我們再把static去掉:
這並不會有任何問題。兩相對比下,可以看出static方法並不認識泛型,所以我們要加上一個<T>,告訴static方法,後面的T是一個泛型。既然static方法不認識泛型,那我們看一下static變數是否認識泛型:
這證明了,static變數也不認識泛型,其實不僅僅是static方法、static變數,static塊也不認識泛型,可以自己試一下。總結起來就是一句話:靜態資源不認識泛型。
泛型約束
可以對泛型引數作約束,本來覺得應該有規律,後來發現沒有,那就把自己研究的結論發一下,假設有一組類繼承關係C繼承自B,B繼承自A:
1、定義class的時候只能使用extends關鍵字且不能用萬用字元"?"
public class TestMain<T extends B> { public static void main(String[] args) { new TestMain<C>(); } }
正確。TestMain類的泛型只能傳B的子類,也就是C。"new TestMain<A>()"、"public class TestMain<? extends B>"、"public class TestMain<T super B>"都是錯誤的寫法
2、作為方法的引數,泛型可以使用"? extends B"或者"? super B",前者表示實際型別只可以是B的子類,後者表示實際型別只可以是B的父類,以下兩種寫法都是正確的:
public static void main(String[] args) { print(new ArrayList<C>()); } public static void print(List<? extends B> list) { }
public static void main(String[] args) { print(new ArrayList<A>()); print(new ArrayList<Object>()); } public static void print(List<? super B> list) { }
3、作為區域性變數的引數,泛型可以使用"? extends B"或者"? super B",不過前者好像沒什麼意義,後者表示只可以傳以B為父類的物件,所以以下的寫法是正確的:
public static void main(String[] args) { List<? super B> list = new ArrayList<B>(); list.add(new C()); }
不要寫"list.add(new A())",JDK將會認為這是型別不匹配的。