天天看點

泛型的常見問題及概念

為什麼會引入泛型?

泛型的本質是為了參數化類型(在不建立新的類型的情況下,通過泛型指定的不同類型來控制形參具體限制的類型)。也就是說在泛型使用過程中,操作的資料類型被指定為一個參數,這種參數類型可以用在類、接口和方法中,分别被稱為泛型類、泛型接口、泛型方法。

場景一:數字求和

常用的資料類型會有很多種,例如int,double等等,那使用泛型的邏輯寫一個公共的求和方法怎麼實作呢?

package com.springtest.generic;

/**
 * 測試泛型使用
 *
 * @author huzhibo
 * @version [1.0, 10:20 2021/08/03]
 */
public class GenericNumberUtils {
    /**
     * 計算兩數之和
     * @param t1 數字1
     * @param t2 數字2
     * @param <T> 兩個數的類型
     * @return 和值
     */
    public static <T extends Number> double add(T t1,T t2){
        return t1.doubleValue() + t2.doubleValue();
    }
}

           

寫一個測試類,測試該方法

package com.springtest.generic;

import junit.framework.TestCase;
import org.junit.Test;

/**
 * 泛型測試
 *
 * @author huzhibo
 * @version [1.0, 10:24 2021/08/03]
 */
public class GenericNumberUtilsTest {

    @Test
    public void addTest(){
        Integer i1 = 10;
        Integer i2 = 20;
        System.out.println(GenericNumberUtils.add(i1, i2));
    }
    @Test
    public void addTest2(){
        double i1 = 10;
        double i2 = 20;
        System.out.println(GenericNumberUtils.add(i1, i2));
    }
    @Test
    public void addTest3(){
        Integer i1 = 10;
        double i2 = 20;
        System.out.println(GenericNumberUtils.add(i1, i2));
    }


}
           

列印的結果為:

30.0
30.0
30.0
           

泛型類及泛型接口

單元泛型

package com.springtest.generic;

import lombok.Data;

/**
 * 泛型類
 *
 * @author huzhibo
 * @version [1.0, 10:48 2021/08/03]
 */
@Data
public class Point<T> {
    private T t;
}

           

多元泛型

package com.springtest.generic;

import lombok.Data;

/**
 * 多元泛型類
 * @author huzhibo
 * @version [1.0, 10:52 2021/08/03]
 */
@Data
public class MultipleGeneric<T,K> {

    private T t;
    private K k;
}

           

泛型接口

package com.springtest.generic;

/**
 * 泛型接口
 *
 * @author huzhibo
 * @version [1.0, 10:57 2021/08/03]
 */
public interface GenericInterface<T> {
    T getT();
}

package com.springtest.generic;


/**
 * 泛型接口實作類
 *
 * @author huzhibo
 * @version [1.0, 10:57 2021/08/03]
 */
public class GenericInterfaceImpl<T> implements GenericInterface<T>{
    private T t;

    @Override
    public T getT() {
        return this.t;
    }
    public void setT(T t){
        this.t = t;
    }
    public GenericInterfaceImpl(T t){
        this.setT(t);
    }
}

           

測試類如下:

@Test
    public void classTest(){
        Point<Integer> integerPoint = new Point<>();
        integerPoint.setT(10);
        Integer t = integerPoint.getT();
        System.out.println(t);
    }
    @Test
    public void multipleGenericTest(){
        MultipleGeneric<String, Integer> multipleGeneric = new MultipleGeneric<>();
        multipleGeneric.setT("測試");
        multipleGeneric.setK(20);
        System.out.println(multipleGeneric);
    }
    @Test
    public void genericInterfaceTest(){
        GenericInterface<String> genericInterface = new GenericInterfaceImpl<String>("測試");
        System.out.println(genericInterface.getT());
    }
           

泛型上下限

<?> 無限制通配符
<? extends E> extends 關鍵字聲明了類型的上界,表示參數化的類型可能是所指定的類型,或者是此類型的子類
<? super E> super 關鍵字聲明了類型的下界,表示參數化的類型可能是指定的類型,或者是此類型的父類

// 使用原則《Effictive Java》
// 為了獲得最大限度的靈活性,要在表示 生産者或者消費者 的輸入參數上使用通配符,使用的規則就是:生産者有上限、消費者有下限
1. 如果參數化類型表示一個 T 的生産者,使用 < ? extends T>;
2. 如果它表示一個 T 的消費者,就使用 < ? super T>;
3. 如果既是生産又是消費,那使用通配符就沒什麼意義了,因為你需要的是精确的參數類型。

           

示例一就是上限的一種使用。

下限的使用如下

public static void fun(Point<? super String> temp){    // 隻能接收String或Object類型的泛型,String類的父類隻有Object類
        System.out.print(temp + ", ") ;
    }
    
    
      @Test
    public void superTest(){
        Point<String> stringPoint = new Point<>();
        stringPoint.setT("測試");
        GenericNumberUtils.fun(stringPoint);

        Point<Object> objectPoint = new Point<>();
        objectPoint.setT(new Object());
        GenericNumberUtils.fun(objectPoint);
    }
           

多個泛型條件也可以使用&符号 如:<T extends Staff & Passenger>

泛型擦除

消除類型參數聲明,即删除

<>

及其包圍的部分。

根據類型參數的上下界推斷

并替換所有的類型參數為原生态類型:如果類型參數是無限制通配符或沒有上下界限定則替換為Object,如果存在上下界限定則根據子類替換原則取類型參數的最左邊限定類型(即父類)。

為了保證類型安全,必要時插入強制類型轉換代碼。

自動産生“橋接方法”以保證擦除類型後的代碼仍然具有泛型的“多态性”。

  • 擦除類定義中的類型參數 - 無限制類型擦除

當類定義中的類型參數沒有任何限制時,在類型擦除中直接被替換為Object,即形如

<T>

<?>

的類型參數都被替換為Object。

  • 擦除類定義中的類型參數 - 有限制類型擦除

當類定義中的類型參數存在限制(上下界)時,在類型擦除中替換為類型參數的上界或者下界,比如形如

<T extends Number>

<? extends Number>

的類型參數被替換為

Number

<? super Number>

被替換為Object。

  • 擦除方法定義中的類型參數

擦除方法定義中的類型參數原則和擦除類定義中的類型參數是一樣的,這裡僅以擦除方法定義中的有限制類型參數為例。

常見問題場景

Oracle官網提供的一個例子:

List<String>[] lsa = new List<String>[10]; // Not really allowed.
Object o = lsa;
Object[] oa = (Object[]) o;
List<Integer> li = new ArrayList<Integer>();
li.add(new Integer(3));
oa[1] = li; // Unsound, but passes run time store check
String s = lsa[1].get(0); // Run-time error ClassCastException.

           

由于 JVM 泛型的擦除機制,是以上面代碼可以給

oa[1]

指派為 ArrayList 也不會出現異常,但是在取出資料的時候卻要做一次類型轉換,是以就會出現

ClassCastException

,如果可以進行泛型數組的聲明則上面說的這種情況在編譯期不會出現任何警告和錯誤,隻有在運作時才會出錯,但是泛型的出現就是為了消滅

ClassCastException