一文搞懂泛型 泛型

泛型

泛型是什麼?

泛型,即“參數化類型”。類型像參數一樣,具有多種類型,在使用時才確定。
比如我們需要一個裝 int 類型的容器,和一個裝 String 類型的容器,要分別製造幾個容器嗎?比如 IntArrayList 和 StringArrayList ,這樣就需要無數個容器了,這種場景就需要泛型。

List<Integer> intList = new ArrayList();
List<String> strList = new ArrayList();

參數化類型意味着可以通過執行泛型類型調用時分配一個類型,用分配的具體類型替換泛型類型。下面 ArrayList 中<E>就是泛型類型,在使用時才分配具體類型:

public class ArrayList<E> extends AbstractList<E>

通俗的說,就是我要一個籃子,可能用這個籃子裝水果,那它就是一個水果籃,也可能水果喫完後用來裝垃圾,那它就是垃圾籃,沒必要寫死這個籃子只能裝水果或者垃圾,但是裝水果的時候不希望籃子裏有垃圾,裝垃圾的時候不希望有水果。這時候就要泛型了。

泛型的好處

  1. 提高安全性: 將運行期的錯誤轉換到編譯期. 如果我們在對一個對象所賦的值不符合其泛型的規定, 就會編譯報錯.
  2. 避免強轉: 比如我們在使用List時, 如果我們不使用泛型, 當從List中取出元素時, 其類型會是默認的Object, 我們必須將其向下轉型爲String才能使用。比如:
List l = new ArrayList();
l.add("abc");
String s = (String) l.get(0);

而使用泛型,就可以保證存入和取出的都是String類型, 不必在進行cast了,也可以直接調用類型獨有的方法,比如:

List<String> l = new ArrayList();
l.add("abc");
l(0).split("b");

如何使用泛型

類型參數用作佔位符,在運行時爲類分配類型。根據需要,可能有一個或多個類型參數,根據慣例,類型參數是單個大寫字母,該字母用於指示所定義的參數類型。下面列出每個用例的標準類型參數:

  • E:元素
  • K:鍵
  • N:數字
  • T:類型
  • V:值
  • S、U、V 等:多參數情況中的第 2、3、4 個類型
public class Test<T> {

    private T obj;

    public T getObj() {
        return obj;
    }

    public void setObj(T obj) {
        this.obj = obj;
    }
}

使用:

    Test<String> t = new Test<>();
        t.setObj("abc");

在JDK1.7時就推出了一個新特性叫菱形泛型(The Diamond), 就是說後面的泛型可以省略直接寫成<>, 反正前後一致。

泛型中的通配符

?和關鍵字extends或者super在一起其實就是泛型的高級應用:通配符。

固定上邊界通配符 <? extends E>

interface Fruit {

     double getWeight();
}

class Apple implements  Fruit{

    @Override
    public double getWeight() {
        return 5;
    }
}
class Orange implements  Fruit{

    @Override
    public double getWeight() {
        return 4;
    }
}

加入小明買了一個果籃,可以裝蘋果,也可以裝橘子。

        ArrayList<Fruit> fruits = new ArrayList<>();
        Apple apple = new Apple();
        fruits.add(apple);
        Orange orange = new Orange();
        fruits.add(orange);

但是小明只想讓這個籃子裝橘子:

ArrayList<Fruit> fruits = new ArrayList<Orange>(); // 編譯報錯

這樣編輯器就報錯了,因爲泛型不支持向上轉型。那怎麼實現小明的需求呢:

ArrayList<? extends Fruit> fruits = new ArrayList<Orange>();

這樣編輯器就不報錯了,但是問題又來了,這個籃子不能裝東西,會報錯:

        ArrayList<? extends Fruit> fruits = new ArrayList<Orange>();
        fruits.add(orange); // 編譯報錯

這是因爲,如果可以裝水果,那並不知道你裝的是蘋果還是橘子,如果你裝了蘋果,小明女朋友想喫橘子,取出的是蘋果,小明女朋友就要分手了。

編輯器也爲了我們想取蘋果的時候取出橘子,導致類型錯誤,所以不允許調用 addset,等參數是泛型的方法。

那這個籃子什麼用呢?其實,還真有用,比如小明要給水果稱重,你不知道是蘋果籃還是橘子籃,所以這樣寫:

    static double getWeight(List<? extends Fruit> list) {

        double weight = 0;
        for (int i = 0; i < list.size(); i++) {
            weight += list.get(i).getWeight();
        }
        return weight;
    }
        ArrayList<Orange> oranges = new ArrayList<>();
        oranges.add(orange);
        ArrayList<Apple> apples = new ArrayList<>();
        apples.add(apple);
        getWeight(apples);
        getWeight(oranges);

這樣就可以給水果稱重了。

<? extends E>就是固定上界通配符
重點說明:我們不能對List<? extends E>使用add方法。原因是,我們不確定該List的類型, 也就不知道add方法的參數類型。
但是也有特例,可以添加null

    ArrayList<? extends Fruit> fruits = new ArrayList<Orange>();
        fruits.add(null);

固定下邊界通配符 <? super E>

小明女朋友喜歡橘子,也喜歡喝橙汁,小明送了2個橘子,一個放果籃裏準備生喫,一個放廚房籃子做果汁:

interface Fruit {

    double getWeight();
}

interface Juice {

}

class Orange implements Fruit,Juice {

    @Override
    public double getWeight() {
        return 4;
    }
    public void addList(List<? super Orange> list) {
        list.add(this);
    }
}

        List<Juice> juices = new ArrayList<>();
        List<Fruit> fruits = new ArrayList<>();

        Orange orange1 = new Orange();
        Orange orange2 = new Orange();
        orange1.addList(juices);
        orange2.addList(fruits);
        Fruit object = fruits.get(0);

小明女朋友想要裝籃子的時候,取出第一個:

    public void addList(List<? super Orange> list) {
        Juice object = list.get(0); // 編譯報錯
        list.add(this);
    }

竟然報錯了,並不知道是水果籃子還是橙汁籃子。萬一小明女朋友想從水果籃子拿出一個,結果拿的是橙子籃子的,那麼小明又要被分手了。

重點說明:我們不能對List<? super E>使用 get 方法。
原因是,我們不確定該List的類型, 也就不知道 get 方法的參數類型。
但是也有特例, Object 類型就可以:

    public void addList(List<? super Orange> list) {
        Object object = list.get(0);
        list.add(this);
    }

無邊界通配符 <?>

無邊界的通配符的主要作用就是讓泛型能夠接受未知類型的數據。
 
小明女朋友有個籃子,沒有告訴小明是裝什麼的,於是小明用來裝了橙子:

        List<?> fruits = new ArrayList<Orange>();
        fruits.add(orange); // 編譯報錯
        fruits.get(0); // 編譯報錯

小明又被分手了,why?
小明事後想到:這個籃子可能是小明女朋友裝髒襪子的

<?>就是無邊界通配符,它具有上邊界和下邊界的限制,不能 add 也不能 get 。因爲不能確定類型。

但是也有特例,就是可以 get 到 Object,也可以存入 null。

總結

泛型限定符有一描述:上界不存下界不取。

上界不存的原因:例如 List,編譯器只知道容器內是 Fruit 及其子類,具體是什麼類型並不知道,編譯器在看到 extends 後面的 Fruit 類,只是標上一個 CAP#1 作爲佔位符,無論往裏面插什麼,編譯器都不知道能不能和 CAP#1 匹配,所以就不允許插入。

下界不取的原因:下界限定了元素的最小粒度,實際上是放鬆了容器元素的類型控制。例如 List, 元素是 Orange,可以存入 Orange 及其超類。但編譯器並不知道哪個是 Orange 的超類,如 Juice。讀取的時候,自然不知道是什麼類型,只能返回 Object,這樣元素信息就全部丟失了。

kotlin 中的泛型

和 Java 泛型一樣,Kolin 中的泛型也有通配符:

  • 使用關鍵字 out 來支持協變,等同於 Java 中的上界通配符 ? extends。
  • 使用關鍵字 in 來支持逆變,等同於 Java 中的下界通配符 ? super。

泛型方法和類型推斷

小明的前女友柳巖只吃橘子這一種水果,會收各種禮物,因爲禮物類型不確定,所以收禮物需要泛型方法,爲什麼不用Obje呢?因爲泛型方法具有類型推斷,不用強轉,避免類型轉換異常。

interface GirlFriend<T> {

    T eatFruit(T i);

    <E> E getGift(E e);
}


class LiuYan<T> implements GirlFriend<T> {


    @Override
    public T eatFruit(T t) {
        return t;
    }

    @Override
    public <E> E getGift(E e) {
        return null;
    }
}
        GirlFriend<Orange> liuyan = new LiuYan<>();
        liuyan.eatFruit(new Orange());
        Apple gift = liuyan.getGift(new Apple());

送柳巖一個蘋果,因爲有類型推斷,所以 Apple 不要強轉。

理解嵌套

小明把前女友分類,愛喫水果的分一類:


interface GirlFriend<T extends Fruit> {

    T eatFruit(T i);

    <E> E getGift(E e);
}


class LiuYan<T extends Fruit> implements GirlFriend<T> {


    @Override
    public T eatFruit(T t) {
        return t;
    }

    @Override
    public <E> E getGift(E e) {
        return null;
    }
}

List<? extends  GirlFriend<? extends Fruit>> list = new ArrayList<? extends  GirlFriend<? extends Fruit>>(); // 編譯報錯

列表右邊和左邊一樣,報錯了,右邊去掉 ?:

List<? extends  GirlFriend<? extends Fruit>> list = new ArrayList< GirlFriend<? extends Fruit>>(); 

不報錯了,爲什麼呢?

泛型實例化的時候要確定類型,所以 List 實例化的時候要確定具體類型, GirlFriend 代表這是個女朋友列表。那爲什麼 GirlFriend 後面的泛型卻可以不確定呢?因爲這個列表是女朋友列表,但是那種類型的女朋友列表不管。在裝進去的時候才確定。

類型擦除

        List<String> list1 = new ArrayList<>();
        List<Integer> list2 = new ArrayList<>();
        System.out.println(list1.getClass()==list2.getClass());

上面輸出是 true,因爲虛擬機只會看到 List,泛型被擦除了。

    public class ObjectContainer<T> {
        private T contained;
        public ObjectContainer(T contained) {
            this.contained = contained;
        }
        public T  getContained() {
            return contained;
        }
    }

這段代碼,編譯器會生成以下代碼:

public class ObjectContainer {
    private Object contained;
public ObjectContainer(Object contained) {
    this.contained = contained;
}
public Object getContained() {
    return contained;
}
}

爲什麼會有泛型擦除呢?是因爲泛型的支持是在JDK1.5之後,那麼以前的版本運行時JVM是不能識別泛型的,所以有了一個擦除機制,擦除之後,類型信息轉爲它的邊界類型。

擦除會帶來2個問題,那就是繼承中方法的重載問題,還有就是類型信息擦除之後如何獲取信息的問題。

     void putList(List<Integer> list){

    }
     void putList(List<String> list){

    }

因爲泛型擦除,所以上面兩個方法簽名一致,重載失敗,編譯器報錯。

class Pair<T> {  
    private T value;  
    public T getValue() {  
        return value;  
    }  
    public void setValue(T value) {  
        this.value = value;  
    }  
}

用一個子類繼承:

class DateInter extends Pair<Date> {  
    @Override  
    public void setValue(Date value) {  
        super.setValue(value);  
    }  
    @Override  
    public Date getValue() {  
        return super.getValue();  
    }  
} 

那麼問題來了,不是泛型擦除了嗎?
編譯後的 Pair,

class Pair {  
    private Object value;  
    public Object getValue() {  
        return value;  
    }  
    public void setValue(Object  value) {  
        this.value = value;  
    }  
}

子類重寫的方法:

@Override  
public void setValue(Date value) {  
    super.setValue(value);  
}  
@Override  
public Date getValue() {  
    return super.getValue();  
} 

方法重寫的話,要求參數返回值一致,爲什麼子類會重寫成功呢?

class DateInter extends Pair {

    // 我們重寫的方法
    public void setValue(Date value) {
        super.setValue(value);
    }
    
    // 我們重寫的方法
    public Date getValue() {
        return (Date) super.getValue();
    }
        
    // 虛擬機生成的橋接方法
    @Override
    public Object getValue() {
        return getValue();
    }

    // 虛擬機生成的橋接方法
    @Override
    public void setValue(Object value) {
        setValue( (Date)value);
    }
}

從編譯的結果來看,我們本意重寫setValue和getValue方法的子類,竟然有4個方法,其實不用驚奇,最後的兩個方法,就是編譯器自己生成的橋方法。可以看到橋方法的參數類型都是Object,也就是說,子類中真正覆蓋父類兩個方法的就是這兩個我們看不到的橋方法。而打在我們自己定義的 setvalue和getValue方法上面的@Oveerride只不過是假象。而橋方法的內部實現,就只是去調用我們自己重寫的那兩個方法。

並且,還有一點也許會有疑問,子類中的橋方法 Object getValue()和Date getValue()是同 時存在的,可是如果是常規的兩個方法,他們的方法簽名是一樣的,也就是說虛擬機根本不能分別這兩個方法。如果是我們自己編寫Java代碼,這樣的代碼是無法通過編譯器的檢查的,但是虛擬機卻是允許這樣做的,因爲虛擬機通過參數類型和返回類型來確定一個方法,所以編譯器爲了實現泛型的多態允許自己做這個看起 來“不合法”的事情,然後交給虛擬器去區別。

這樣就巧妙的解決了重載的問題。

泛型類型獲取

List<Fruit> ps = gson.fromJson(str, new TypeToken<List<Fruit>>(){}.getType());  

既然類型擦除了,爲什麼 Gson 在轉 json 的時候還能獲取到?

        List<Fruit> list  = new ArrayList<Fruit>();

我們獲取到的 class 類型是 List,因爲 ArrayList<E> 這個類並沒有類型。但是我們寫個子類繼承 ArrayList<E>,就能獲取子類的類型:
     class FruitArrayList extends ArrayList<Fruit>{}

        List<Fruit> list  = new ArrayList<Fruit>();
        List<Fruit> list2  = new FruitArrayList();

list2 的類型是 FruitArrayList,在看下面的 list3 :

        List<Fruit> list  = new ArrayList<Fruit>();
        List<Fruit> list2  = new FruitArrayList();
        List<Fruit> list3  = new ArrayList<Fruit>(){};

list3 和 list1 的區別是後面有個中括號,代表這個就是一個 ArrayList<Fruit> 的類,就能獲取這個類型。所以 Gson 在轉化的時候,是一樣的方法:

new TypeToken<List<Fruit>>(){}.getType()

如果改爲

new TypeToken<List<Fruit>>().getType()

就獲取不到類型,當然編輯器就報錯了。

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