Java 泛型中的通配符

本文內容如下:

1、 什麼是類型擦除
2、常用的 ?, T, E, K, V, N的含義
3、上界通配符 < ?extends E>
4、下界通配符 < ?super E>
5、什麼是PECS原則
6、通過一個案例來理解 ?和 T 和 Object 的區別

一、什麼是類型擦除?

我們說Java的泛型是偽泛型,那是因為泛型信息只存在於代碼編譯階段,在生成的位元組碼中是不包含泛型中的類型信息的,使用泛型的時候加上類型參數,在編譯器編譯的時候會去掉,這個過程為類型擦除。

泛型是Java 1.5版本才引進的概念,在這之前是沒有泛型的,但是因為類型擦除特性,讓泛型代碼能夠很好地和之前版本的代碼兼容。

我們來看個案例

(圖1)

因為這裡泛型定義為Integer類型集合,所以添加String的時候在編譯時期就會直接報錯。

那是不是就一定不能添加了呢?答案是否定的,我們可以通過Java泛型中的類型擦除特點及反射機制實現。

如下

    public static void main(String[] args) throws Exception {
        ArrayList<Integer> list = new ArrayList();
        list.add(6);
        //反射機制實現
        Class<? extends ArrayList> clazz = list.getClass();
        Method add = clazz.getDeclaredMethod("add", Object.class);
        add.invoke(list, "歡迎關注:後端元宇宙");
        System.out.println("list = " + list);
    }

運行結果

list = [6, 歡迎關注:後端元宇宙]

二、案例實體準備

這裡先建幾個實體,為後面舉例用

Animal類

@Data
@AllArgsConstructor
public class Animal {

    /**
     * 動物名稱
     */
    private String name;

    /**
     * 動物毛色
     */
    private String color;
}

Pig類 :Pig是Animal的子類

public class Pig  extends  Animal{
    public Pig(String name,String color){
        super(name,color);
    }
}

Dog類: Dog也是Animal的子類

public class Dog extends Animal {

    public Dog(String name,String color){
        super(name,color);
    }
}

三、常用的 ?, T, E, K, V, N的含義

我們在泛型中使用通配符經常看到T、F、U、E,K,V其實這些並沒有啥區別,我們可以選 A-Z 之間的任何一個字母都可以,並不會影響程序的正常運行。

只不過大家心照不宣的在命名上有些約定:

  • T (Type) 具體的Java類
  • E (Element)在集合中使用,因為集合中存放的是元素
  • K V (key value) 分別代表java鍵值中的Key Value
  • N (Number)數值類型
  • ? 表示不確定的 Java 類型

四、上界通配符 < ? extends E>

語法:<? extends E>

舉例:<? extends Animal> 可以傳入的實參類型是Animal或者Animal的子類

兩大原則

  • add:除了null之外,不允許加入任何元素!
  • get:可以獲取元素,可以通過E或者Object接受元素!因為不管存入什麼數據類型都是E的子類型

示例

  public static void method(List<? extends Animal> lists){
        //正確 因為傳入的一定是Animal的子類
        Animal animal = lists.get(0);
        //正確 當然也可以用Object類接收,因為Object是頂層父類
        Object object = lists.get(1);
        //錯誤 不能用?接收
        ? t = lists.get(2);
        // 錯誤
        lists.add(new Animal());
        //錯誤
        lists.add(new Dog());
        //錯誤 
        lists.add(object);
        //正確 除了null之外,不允許加入任何元素!
        lists.add(null);
    }

五、下界通配符 < ? super E>

語法: <? super E>

舉例 :<? super Dog> 可以傳入的實參的類型是Dog或者Dog的父類類型

兩大原則

  • add:允許添加E和E的子類元素!
  • get:可以獲取元素,但傳入的類型可能是E到Object之間的任何類型,也就無法確定接收到數據類型,所以返回只能使用Object引用來接受!如果需要自己的類型則需要強制類型轉換。

示例

  public static void method(List<? super Dog> lists){
        //錯誤 因為你不知道?到底啥類型
        Animal animal = lists.get(0);
        //正確 只能用Object類接收
        Object object = lists.get(1);
        //錯誤 不能用?接收
        ? t = lists.get(2);
        //錯誤
        lists.add(object);
        //錯誤
        lists.add(new Animal());
        //正確
        lists.add(new Dog());
        //正確 可以存放null元素
        lists.add(null);
    }

六、什麼是PECS原則?

PECS原則:生產者(Producer)使用extends,消費者(Consumer)使用super。

原則

  • 如果想要獲取,而不需要寫值則使用” ? extends T “作為數據結構泛型。
  • 如果想要寫值,而不需要取值則使用” ? super T “作為數據結構泛型。

示例-

public class PESC {
    ArrayList<? extends Animal> exdentAnimal;
    ArrayList<? super Animal> superAnimal;
    Dog dog = new Dog("小黑", "黑色");

    private void test() {
        //正確 
        Animal a1 = exdentAnimal.get(0);
        //錯誤 
        Animal a2 = superAnimal.get(0);

        //錯誤 
        exdentAnimal.add(dog);
        //正確 
        superAnimal.add(dog);
    }
}

示例二

Collections集合工具類有個copy方法,我們可以看下源碼,就是PECS原則。

   public static <T> void copy(List<? super T> dest, List<? extends T> src) {
        int srcSize = src.size();
        if (srcSize > dest.size())
            throw new IndexOutOfBoundsException("Source does not fit in dest");

        if (srcSize < COPY_THRESHOLD ||
            (src instanceof RandomAccess && dest instanceof RandomAccess)) {
            for (int i=0; i<srcSize; i++)
                dest.set(i, src.get(i));
        } else {
            ListIterator<? super T> di=dest.listIterator();
            ListIterator<? extends T> si=src.listIterator();
            for (int i=0; i<srcSize; i++) {
                di.next();
                di.set(si.next());
            }
        }
    }

我們按照這個源碼簡單改造下

public class CollectionsTest {
    
    /**
     * 將源集合數據拷貝到目標集合
     *
     * @param dest 目標集合
     * @param src  源集合
     * @return 目標集合
     */
    public static <T> void copy(List<? super T> dest, List<? extends T> src) {
        int srcSize = src.size();
        for (int i = 0; i < srcSize; i++) {
            dest.add(src.get(i));
        }
    }
    
    public static void main(String[] args) {
        ArrayList<Animal> animals = new ArrayList();
        ArrayList<Pig> pigs = new ArrayList();
        pigs.add(new Pig("黑豬", "黑色"));
        pigs.add(new Pig("花豬", "花色"));

        CollectionsTest.copy(animals, pigs);
        System.out.println("dest = " + animals);
    }
    
}

運行結果

dest = [Animal(name=黑豬, color=黑色), Animal(name=花豬, color=花色)]

七、通過一個案例來理解 ?和 T 和 Object 的區別

1、實體轉換

我們在實際開發中,經常進行實體轉換,比如SO轉DTO,DTO轉DO等等,所以需要一個轉換工具類。

如下示例

/**
 *  實體轉換工具類
 *  
 *  TODO 說明該工具類不能直接用於生產,因為為了代碼看去清爽點,我少了一些必要檢驗,所以如果直接拿來使用可以會在某些場景下會報錯。
 */
public class EntityUtil {

  
    /**
     * 集合實體轉換
     *
     * @param target 目標實體類
     * @param list   源集合
     * @return 裝有目標實體的集合
     */
    public static <T> List<T> changeEntityList(Class<T> target, List<?> list) throws Exception {
        if (list == null || list.size() == 0) {
            return null;
        }
        List<T> resultList = new ArrayList<T>();
        //用Object接收
        for (Object obj : list) {
            resultList.add(changeEntityNew(target, obj));
        }
        return resultList;
    }

    /**
     * 實體轉換
     *
     * @param target 目標實體class對象
     * @param baseTO 源實體
     * @return 目標實體
     */
    public static <T> T changeEntity(Class<T> target, Object baseTO) throws Exception{
        T obj = target.newInstance();
        if (baseTO == null) {
            return null;
        }
        BeanUtils.copyProperties(baseTO, obj);
        return obj;
    }
}

使用工具類示例

  private void  changeTest() throws Exception {
        ArrayList<Pig> pigs = new ArrayList();
        pigs.add(new Pig("黑豬", "黑色"));
        pigs.add(new Pig("花豬", "花色"));
        //實體轉換
        List<Animal> animals = EntityUtil.changeEntityList(Animal.class, pigs);
    }

這是一個很好的例子,從這個例子中我們可以去理解 ?和 T 和 Object的使用場景。

我們先以集合轉換來說

 public static <T> List<T> changeEntityListNew(Class<T> target, List<?> list);
    

首先其實我們並不關心傳進來的集合內是什麼對象,我們只關係我們需要轉換的集合內是什麼對象,所以我們傳進來的集合就可以用List<?>表示任何對象的集合都可以。

返回呢,這裡指定的是Class<T>,也就是返回最終是List<T>集合。

再以實體轉換方法為例

public static <T> T changeEntityNew(Class<T> target, Object baseTO)

同樣的,我們並不關心源對象是什麼,我們只關心需要轉換的對象,只需關心需要轉換的對象為T

那為什麼這裡用Object上面用?呢,其實上面也可以改成List<Object> list,效果是一樣的,上面List<?> list在遍歷的時候最終不就是用Object接收的嗎

?和Object的區別

?類型不確定和Object作用差不多,好多場景下可以通用,但?可以縮小泛型的範圍,如:List<? extends Animal>,指定了範圍只能是Animal的子類,但是用List<Object>,沒法做到縮小範圍。

總結

  • 只用於功能時,泛型結構使用<? extends T>
  • 只用於功能時,泛型結構使用<? super T>
  • 如果既用於,又用於操作,那麼直接使用<T>
  • 如果操作與泛型類型無關,那麼使用<?>

聲明: 公眾號如需轉載該篇文章,發表文章的頭部一定要 告知是轉至公眾號: 後端元宇宙。同時也可以問本人要markdown原稿和原圖片。其它情況一律禁止轉載!