泛型之通配符

數組 VS List集合

數組類型爲Object,可以存儲任意類型的對象,List集合同樣可以做到

Object[] obj = new Object[1];
List list = new ArrayList();

數組類型可以爲Integer類型,專門存儲Integer類型對象,List集合同樣也可以

String[] str = new String[1];
List<String> list = new ArrayList<>();

前兩局雙方打成平手,最後的決勝局。先看數組代碼:

Object[] obj = new Integer[2];
obj[0] = 52021;
//編譯期OK,運行期就掛了
obj[1] = "Full of confidence";

上面的代碼在運行期會拋出異常:java.lang.ArrayStoreException: java.lang.String

List集合代碼:

//編譯期報錯
List<Object> obj = new ArrayList<String>();

最終結果,List集合更勝一籌,因爲它能儘早的發現錯誤。

分析最後一戰中數組的代碼:

在編譯期間,編譯器不會檢查這樣的賦值,編譯器覺得它是合理的,因爲父類引用指向子類對象,沒有任何問題。賦值時,將字符串存儲在一個Object類型的數組中也說的過去,但在運行時,發現明明內存分配的是存儲整型類型對象的格子,裏面卻放着字符串類型的對象,自然會報錯了。

分析最後一戰中List集合的代碼:

由於運用了泛型,在編譯期就發現了錯誤,避免了將問題帶到運行時。
思考個問題,如果代碼在編譯期沒有報錯會發生什麼?

在編譯期沒有報錯並且在運行期會將泛型擦除,全部變爲了Object類型。所以執行obj.add(new Integer(1))也是可以的。如果真是這樣的話,那麼泛型還有什麼存在的意義呢?所以這種假設是不存在的,所以會在編譯期報錯。

結論:在使用泛型時,泛型的引用和創建兩端,給出的泛型變量必須相同

通配符

通配符只能出現在等號左面,不能出現在new的一邊。

  • List<?> list = new ArrayList<String>()
  • List<? extends Number> obj1 = new ArrayList<Integer>()
  • List<? super Integer> obj = new ArrayList<Number>()

無界通配符

public void foo() {
    List<Integer> list = new ArrayList<Integer>();
    //foo2(list); //編譯期報錯
    foo3(list); //正常編譯
}

public void foo2(List<Object> list) {//TODO}
public void foo3(List<Integer> list){//TODO}

上面代碼中,調用foo2方法編譯期報錯原因:泛型的引用和創建兩端,給出的泛型變量不一致,相當於:List<Object> list = new ArrayList<Integer>();

public void foo() {
    List<Integer> list = new ArrayList<Integer>();
    List<String> strList = new ArrayList<String>();
    foo3(list); //正常編譯
}
public void foo3(List<Integer> list){//TODO}

現在希望將strList作爲參數調用foo3方法,這時就想到了方法的重載,所以定義了一個重載的方法。public void foo3(List<String> list){//TODO}
定義完成後,竟然報錯了,並且foo3(List<Integer> list)也報錯了。這是由於泛型擦除導致的。在運行期,會有泛型擦除,所以foo3(List<Integer> list)foo3(List<String> list)會變成一樣的方法,所以在編譯期就要報錯,否則在運行期就無法區分了。

這裏無法使用foo3方法重載,除非定義不同名字的方法。除了定義不同名字的方法之外,還可以使用通配符。

public void foo() {
    List<Integer> list = new ArrayList<Integer>();
    List<String> strList = new ArrayList<String>();
    foo3(list);
    foo3(strList);
}
public void foo3(List<?> list){//TODO}

調用foo3(strList);相當於:
List<?> list = new ArrayList<String>(); => List<?> list = strList

通過使用通配符,foo3(List<?> list);可以傳遞任何類型的對象,但也是有缺點的。使用通配符,賦值/傳值的時候方便了,但是對泛型類中參數爲泛型的方法起到了副作用。如何理解泛型類中參數爲泛型的方法起到了副作用這句話呢?結合代碼來看

/**
 * 使用 ? 通配符
 */
public void foo3(List<?> list) {
    //list.add(1); //編譯期報錯,起到了副作用
    //list.add("hello"); //編譯期報錯,起到了副作用
    Object o = list.get(0); //其實也是作廢的,只是由於Object是所有類的父類,所以這裏不會報錯。
}

List定義:接口List<E>E代表是泛型類;List中add方法定義:boolean add(E e),參數爲E,說明參數爲泛型。List接口使用通配符,調用add方法時,副作用是在編譯期報錯;泛型類中返回值爲泛型的方法,也作廢了,如:get方法定義:E get(int index)

子界限定

  • 子界限定:? extends Number
  • 解釋:? 是 Number 類型或者是 Number 的子類型
  • 缺點:參數爲泛型的方法不能使用
public void foo4() {
    List<Integer> intList = new ArrayList<Integer>();
    List<Long> longList = new ArrayList<Long>();
    foo5(intList); //Integer是Number的子類型
    foo5(longList); //Long是Number的子類型
}
public void foo5(List<? extends Number> list) {
    //list.add(1); //編譯期報錯
    //list.add(2L); //編譯期報錯
}

分析以上代碼:

foo5(intList);相當於List<? extends Number> list = intList;賦值操作,這是沒有問題的;list.add(1);則會在編譯期報錯。
list定義的類型是List<? extends Number>,它帶有泛型,而add方法的參數也是泛型類型,符合:泛型類中參數爲泛型的方法起到了副作用這個結論。所以調用add編譯期報錯。

想想看,如果list.add(1);不報錯:

foo4中調用了foo5(longList);相當於List<? extends Number> list = new ArrayList<Long>();,然後執行foo5,調用list.add(1);如果不報錯也就相當於Long類型容器可以盛放Integer類型數據,這樣一來,泛型也就沒有意義了。有人也許會問,既然add方法會報錯,爲什麼foo5(longList);沒有問題?其實我覺得這是不一樣的,調用add方法不確定因素很多,因爲類型可能是Integer,也可能是Long,人們無法保證在調用add方法時,只傳遞相同類型的變量,所以程序就直接限定死了,你不可以add任何東西。但直接賦值,這個值是可以確定的,類型具有統一性,要是什麼都是什麼,所以是可行的。

結論:當使用子界限定通配符時,泛型類中參數爲泛型的方法不能使用

也許有人會問,這樣做是否可以確定類型?

List<? extends Number> list = new ArrayList<Integer>();
list.add(1); //編譯期報錯

很遺憾,這樣也是不行的。? 仍然代表了不確定性,所以編譯器壓根就是將這種方式的方法的參數類型是泛型的全部廢掉了。

add不能用,那賦值操作後可從中取值嗎?答案是肯定的。
Number number = list.get(0);

分析以上代碼:

無論返回什麼值,總歸都是Number類型的,這是可以確定的,所以可以用Number接收返回值爲泛型的方法。當然,這裏說沒有問題也只能是Number類型或者是Object類型接收,別的類型是不可以的。

結論:當使用子界限定通配符時,泛型類中返回值爲泛型的方法可以使用

父界限定

  • 父界限定:? super Integer
  • 解釋:? 是Integer類型或者是Integer的父類型
  • 缺點:返回值爲泛型的方法不能使用
public void foo6() {
    List<Number> numList = new ArrayList<Number>();
    List<Integer> intList = new ArrayList<Integer>();
    foo7(numList); //Number是Integer的父類型
    foo7(intList); //Integer是本身
}
public void foo7(List<? super Integer> list) {
    list.add(1);
}

分析以上代碼:

list.add(1);不會報錯,因爲類型可以確定。

  • 如果List<? super Integer> list的賦值是List<? super Integer> list = new ArrayList<Number>();沒問題
  • 如果List<? super Integer> list的賦值是List<? super Integer> list = new ArrayList<Integer>();沒問題
  • 如果List<? super Integer> list的賦值是List<? super Integer> list = new ArrayList<Object>();也沒問題

所以只要add(1)就肯定沒有問題,就是說add(1),這個實參1,符合任何一種情況。

結論:當使用父界限定通配符時,泛型類中參數爲泛型的方法可以使用

public void foo7(List<? super Integer> list) {
    list.add(1);
    Object obj = list.get(0);
}

Object obj = list.get(0);這句話沒有報錯,但其實是作廢的,只是由於Object是所有類的父類,所以纔可以這麼用。

結論:當使用父界限定通配符時,泛型類中返回值爲泛型的方法不能使用

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章