對Java萬用字元的個人理解(以集合為例)

JNovice發表於2018-07-30

對Java萬用字元的個人理解(以集合為例)


  • 前言:最近在學習Java,當學到了泛型的萬用字元時,不是很理解PECS(Producer Extends Consumer Super)原則,以及<? extends E> 不能使用add方法和<? super E> 不能使用get方法(注意:僅能使用Object o = list.get(0);取得是Object物件),所以我對它進行了學習和理解,這篇博文用了簡單通俗的方法去講解add和get在萬用字元中的使用場景以及PECS原則。可能本人的水平有限,如果我的理解有誤或者內容錯誤,歡迎指出來,我好進行及時修改。

一、什麼是泛型萬用字元


<一>簡單定義泛型統配符

1. 在瞭解泛型的統配符之前,我們先了解下什麼是泛型,泛型是一種包含型別引數的類,值得注意的一點是這裡的型別必須是引用資料型別,而且放在尖括號< >內,這裡引進了型別引數,將類直接作為了引數。


2. 那麼是什麼泛型統配符呢,我沒有找到定義,所以我自己給它下了個定義。泛型萬用字元是在泛型的使用中,用來表示對泛型型別進行型別範圍限定的特殊符號。這裡用萬用字元就是為了表明要輸入的型別要在一定範圍之內,說的通俗一些其實就是一個型別取值範圍,而最大值是Object這是確定的。


<二>泛型萬用字元的分類

1. <?>:無限萬用字元,可以在?中放入裡放入任何引用資料型別,用來接收任意引用資料型別。


2. <? extends E>:這個表明的是萬用字元的上界,通俗講就如同取值範圍中的負無窮到E,即小於等於E的所有型別, 因為E是最大的型別(最大可以達到Object),表明可以輸入所有的E的子類和E,等下會進行細緻的講解。


3. <? super E>:這個表明的是萬用字元的下界,通俗講其取值範圍就是E到最大值Object(因為Object是所有類的基類),就是大於等於E,小於等於Object類。


  • 注意:這裡能制定上界或者下界,但是不能同時制定,然後<? extends E>中的extends不一定表示類與類的繼承還可以表示實現的關係,然後萬用字元一般是用在方法的形參宣告和方法呼叫上,無法用於定義類和介面中。

 

二、泛型萬用字元講解


 <一>萬用字元的使用以及程式碼演示

      1.無限萬用字元<?>的使用:可以傳入任何引用資料型別

 

A 在呼叫方法時使用?萬用字元的過程中無法使用add方法。

原因分析:因為萬用字元?代表任意的資料型別,但是當我們呼叫的時候或者用在方法的宣告上,其實這個時候還是沒有給?萬用字元一個指定的引用資料型別,那麼Java出於安全起見,就不可能允許新增元素。


B 以上的add方法雖然無法呼叫,add(null)是例外。

原因分析:因為null可以給任意引用資料型別賦值,代表任意引用資料,但是很容易引起NullPointerException。


C 注意使用List<?>和List<Object>當作形參時的作用不能等同,比如當傳入List<Integer>時List<?>可以接收,但是List<Object>無法接收。

原因分析:因為?代表任何引數型別可以接收,但是List<Object>中雖然Object是所有子類的父類,但是List<Object>不是List<Integer>的父類,List<Object>是ArrayList<Object>等類的父類,這就是為什麼泛型前後要一致的原因,從陣列的角度去理解集合就比如Object[ ] arr不是Integer[ ] arr1的父類。

 

 1 package com.test;
 2 
 3 import java.util.ArrayList;
 4 import java.util.List;
 5 
 6 public class InterviewList {
 7 
 8     public static void main(String[] args) {
 9 //      ArrayList<Object> list2 = new ArrayList<Integer>();// 泛型前後要一致
10         ArrayList<?> list = new ArrayList<Integer>();
11 //      list.add("122");// 注意此處無法新增"122"字串,但是可以新增null,因為null可以給任何引用資料型別賦值
12         list.add(null);// 新增null沒有報錯
13         Object o = list.get(0);
14         System.out.println(o);
15 
16         ArrayList<Integer> list1 = new ArrayList<Integer>();
17         list1.add(34);
18 //        Error:(18, 24) java: 不相容的型別: java.util.ArrayList<java.lang.Integer>無法轉換為java.util.List<java.lang.Object>
19 //      報錯了,原因因為這裡要接收的是List<Object>,那麼可以接收的有ArrayList<Object>或者List<Object>等與List<Object>有繼承關係的集合,但是無法接收ArrayList<Integer>,因為和List<Object>沒有繼承關係,寫成List<?>才能使用
20 //      printCollection1(list1);
21 
22         printCollection(list1);// ? 萬用字元可以正常接收
23     }
24 
25     public static void printCollection(List<?> col) {//此方法使用了無限萬用字元
26         for (Object object : col) {
27             System.out.println(object);
28         }
29     }
30 
31     public static void printCollection1(List<Object> col) {//此方法沒有使用泛型萬用字元
32         for (Object object : col) {
33             System.out.println(object);
34         }
35     }
36 }
View Code

 

 

 

      2.上界萬用字元<? extends E>的使用:可以傳入E和E的子類

 

A <? extends E>作為形參時例如List<? extends E>可以使用集合的get方法來獲取E或者E型別本身的元素。

原因分析:當我們用get方法時我們其實是在獲取集合裡內部的元素,但是我們的集合的資料型別還沒有確定,但是我們可以獲得一些明確的已知條件,那就是在<? extends E>中最大的型別是E,而且這個E最大是Object,所以我們可以利用這一點,那麼我們就可以清楚地瞭解到該集合裡面的獲取的元素肯定是E或者Object的子類,他們的範圍肯定小於E或者Object,那麼我們就可以用Object和E這兩個範圍比集合裡面的元素大的類去接收集合裡面的元素。(注:可能略顯囉嗦但是我就是想解釋清楚。)


B 在使用上界萬用字元時,無法呼叫add方法來新增非null的元素。

原因分析:由於上面已經說得很清楚了,<? extends E>作為形參時例如List<? extends E>這時最大型別是E和Object,但是我們不清楚最小的型別是什麼,因為此時?這個萬用字元沒有被賦值,我們呼叫add方法是要新增集合元素或者集合元素的子類,但是我們沒法明確肯定該集合元素型別,或者比該集合元素範圍更小的子類,那麼Java就不會允許新增元素。

 

 1 public class WildCardTest2 {
 2 
 3     public static void main(String[] args) {
 4         ArrayList<? extends Number> list = new ArrayList<>();
 5 //        list.add(3);報錯了,無法新增非null元素
 6         list.add(null);//沒有報錯
 7         Object o = list.get(0);//用Object接收沒有報錯
 8         Number n = list.get(0);//用Number接收沒有報錯
 9     }
10 
11 }

 

       3.下界萬用字元<? super E>的使用:可以傳入E或者E的父類

 

A 在使用下界萬用字元時,無法使用get方法獲取Object以外的元素,或者需要向下轉型,但是可能出現ClassCastException的異常。

原因分析:上界萬用字元,在使用get方法的時候,此時型別沒有明確還是問號?我們只能明確其最大父類或者介面時,我們才能接收,但是我們只能明白<? super E>作為形參時例如List<? super E>時,只能明確Object是最大父類,其他的一概不知,所以只能Object o = list.get(0)。


B 可以使用集合的add方法新增E或者E的子類。

原因分析:上界萬用字元已經解釋很清楚了,add方法新增元素時,?型別不確定就要明確該?型別的最小子類,只要比可能存在的最小子類或者子介面小的任意引用資料型別的物件,我們都可以將其新增,而下界萬用字元<? super E>當作形參時例如List<? super E>,此時E就是最小子類,此時add方法可以新增E或者E的子類。

 

 1 public class WildCardTest3 {
 2 
 3     public static void main(String[] args) {
 4         ArrayList<? super Number> list = new ArrayList<>();
 5         list.add(3);//沒有報錯,自動裝箱成Integer,Number的子類
 6         list.add(3.4F);//沒有報錯,自動裝箱成Float,Number的子類
 7         list.add(32L);//沒有報錯,自動裝箱成Long,Number的子類
 8         Object o = list.get(1);
 9 //        Integer i = list.get(0);//報錯了,無法用Integer接收
10     }
11 
12 }

 

<二>對PECS原則的解讀

      1.什麼是PECS原則?

            PECS是Producer Extends Consumer Super的遞迴縮寫,是Java中使用泛型萬用字元的原則。

 

    2.阿里巴巴的萬用字元使用規約


泛型萬用字元<? extends T>來接收返回的資料,此寫法的泛型集合不能使用 add 方法,而 < ? super T> 不能使用 get 方法,做為介面呼叫賦值時易出錯。
說明:擴充套件說一下 PECS<Producer Extends Consumer Super> 原則:第一、頻繁往外讀取內容的,適合用<? extends E>。第二、經常往裡插入的,適合用<? super E> 。

      

      3.對PECS原則的簡單解讀

字面意思是生產者要被繼承要被當作上界萬用字元<? extends E>的上界E,消費者要繼承其他類要被當成下界萬用字元<? super E>的下界E,再借助下阿里巴巴的泛型開發規約去理解下,應該就是當這個被傳入的型別需要進行很多get操作獲取資料的話,那麼請使用上界萬用字元這時這個上界就如同生產者一樣,因為它能被不斷get到,而當需要不斷進行add方法新增資料的話,請使用下界萬用字元這時這個下界就如同消費者一樣,因為它不斷地索取,因為我們在不斷地add元素給它。

相關文章