1. 程式人生 > 其它 >通俗易懂講泛型

通俗易懂講泛型

由於部落格園的 markdown 語法有點坑,格式如果閱讀中遇到問題,可以非同步本人語雀文件檢視:https://www.yuque.com/docs/share/57b89afd-91d8-4e64-82a0-243c74304004?# 《泛型》

泛型是什麼

傳統程式設計大多數都是面向物件型別程式設計,比如方法引數傳入一個指定的型別,這類程式碼比較難複用,通常新增一個型別時就得增或者改程式碼。當然除了面對特定的型別程式設計還有面向基類和介面程式設計的多型程式設計,這類的程式碼會通用一些,方法入參傳入一個基類或者介面,那麼該方法就能適用於基類的所有派生類和介面的實現者,新增派生類也無需更改程式碼,但是這樣就增加了程式碼的耦合度,必須繼承指定基類或者接口才行。泛型就更加通用了,泛型實現了引數化型別,這樣我們編寫的通用方法就可以適用於多種型別,而不是一個具體的介面或者類。

個人理解泛型的主要作用是為了複用元件程式碼,當然在泛型出現之前也是可以編寫通用的元件程式碼的,但是這樣有點不安全。

泛型出來之前,集合記憶體儲的元素是 Object 型別的,Object 是所有類的基類,因此,可以往集合內部新增任意型別,如上例子就往 listOld 集合內添加了:字串,整形,物件,這三種類型。這樣會導致一種問題,我們無法得知集合內的元素究竟是什麼型別的,只能知道他們都是 Object 的子類。這樣在使用的時候就得進行強制型別轉化。使用的不當的話很容易報 “ClassCastException” 異常。

java 5 泛型出來之前,集合的使用方法:

public class Main {
    public static void main(String[] args) {
        List listOld = new ArrayList();
        listOld.add("string");
        listOld.add(123);
        listOld.add(new Main());

        for (Object o : listOld) {
            if (o instanceof String) {
                String str = (String) o;
                System.out.println("this is string type");
            }
            if (o instanceof Integer) {
                Integer i = (Integer) o;
                System.out.println("this is Integer type");
            }
            if (o instanceof Main) {
                Main m = (Main) o;
                System.out.println("this is Object");
            }
        }
    }
}

輸出:

this is string type

this is Integer type

this is Object

泛型出來之後,我們就可以為集合表明一個確定的型別,這樣就可以往集合內新增該型別或者該型別的子類。如果新增的型別不正確,那麼編譯期就會報錯。

通過在集合引入泛型,那麼編譯器就會在編譯器進行型別校驗,如果往一個指定了型別的集合內部添加了錯誤型別,編譯器就會報錯。

而在使用元素的時候,也會自動的將集合內的元素轉化為指定型別,這種轉化是安全的,因為編譯器確保了只能往集合內新增指定型別的元素。

java 5 泛型出來之後,集合的使用方法:

public class Main {
    public static void main(String[] args) {
        List<String> listNew = new ArrayList<>();
        listNew.add("string");
//        listNew.add(123);// 編譯報錯
        for (String s : listNew) {
            System.out.println(s);
        }
    }
}

輸出:

string


那麼,泛型這一套是怎麼實現的呢,這就很有意思,jdk 有個傳統,就是向上相容,也就是說每發行一個版本,老版本的程式碼必定能夠在新版本的 jdk 上執行。因此,為了相容 java 5 之前的集合使用方式,jdk 的研發人員,採用了 “泛型擦除”的方式進行設計。

泛型擦除

老實說,剛開始知道泛型擦除這個概念的時候個人覺得有點拉閘..由於沒使用過 c++ 和 python(我承認我菜..目前還沒有去學習其他程式語言的想法),因此便不知道其他語言是怎麼實現泛型的。

那麼,什麼是泛型擦除呢,泛型擦除是一種面向編譯期設計的方法。所有的我們平時見到的如:

List <String > list;

List <T> list;

List<?> list;

List<? extends Object> list;

List<? super ArrayList> list;

這些泛型語法,經過編譯期,到執行期的時候,統統變成了:

List list

也就是指定的型別統統向上轉型成了 Object,所以我說泛型擦除是一種面向編譯期設計的方法。

那麼知道了泛型是什麼,理解了泛型前後的程式設計規範,並且知道了泛型的實現後,讓咱們來了解了解泛型怎麼使用吧。

泛型怎麼使用

如下例子,很簡單,在建立類的時候在類名右邊使用 尖括號括起來,然後裡面隨便定義個字母即可,這個字母就代表你可傳進來的型別。

public class GenericClass<T> {

    private T obj;

    public T getObj() {
        return obj;
    }

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

    @Override
    public String toString() {
        return "GenericClass{" +
                "obj=" + obj +
                '}';
    }

    public static void main(String[] args) {
        GenericClass<String> demo1 = new GenericClass<>();
        demo1.setObj("demo1");
        String obj = demo1.getObj();
        System.out.println(obj);
        System.out.println(demo1);

        GenericClass<Integer> demo2 = new GenericClass<>();
        demo2.setObj(1);
        Integer obj1 = demo2.getObj();
        System.out.println(obj);
        System.out.println(demo2);
    }
}

輸出:

demo1

GenericClass{obj=demo1}

demo1

GenericClass{obj=1}

萬用字元

泛型有個概念是萬用字元,泛型萬用字元有三種

  • <?>無界萬用字元,接受任意型別,等同於 <Object>

  • <? extends Object >:上界萬用字元,這裡的 Object 可以是任意類,代表的意思是接受任意繼承自 Object 的型別

  • <? super Object > :下界萬用字元,這裡的 Object 可以是任意類,代表的意思是接受任意 Object 的父類

無界萬用字元

無界萬用字元 <?> 看起來意味著“任何事物”,因此使用無界萬用字元好像等價於使用原生型別,但是它仍舊是很有價值的,因為,實際上它是在宣告:“我是想用 Java 的泛型來編寫這段程式碼,我在這裡並不是要用原生型別,但是在當前這種情況下,泛型引數可以持有任何型別。

public class Main {
    public static void main(String[] args) {
        Map map1 = new HashMap();
        Map<String, ?> map2 = new HashMap<String, Main>();
        Map<String, ?> map3 = new HashMap<String, String>();
        Map<String, ?> map4 = new HashMap<Integer, String>();//編譯失敗
        Map<?, ?> map5 = new HashMap<String, String>();
        Map<?, ?> map6 = new HashMap<Integer, Integer>();
    }
}

上界萬用字元

上界萬用字元很有意思,如下程式碼,在 1 處其實會產生編譯報錯,因為上界萬用字元不允許 set 和 add 值,為什麼呢,上界萬用字元的意思是我允許存放所有父類的子類,但是泛型代表的是具體型別,這裡的具體型別畫重點,就像 2 和 3的用法,雖然採用的上界萬用字元,但是裡面的型別是確定的。

如果你想使用不確定型別,那麼直接採用多型特性,比如 4 那樣即可。

public class Main {
    public static void main(String[] args) {
        List<? extends Father> sonList = new ArrayList<>();    // 1
//        sonList.add(new Son());  編譯報錯
        sonList = Arrays.asList(new Son(), new Son());          // 2
        for (Father father : sonList) {
            System.out.println(father.getClass().getName());
        }
        List<? extends Father> daughterList = Arrays.asList(new Daughter(), new Daughter()); // 3

        List<Father> list1 = new ArrayList<>();             // 4
        list1.add(new Son());
    }
}

上界萬用字元也有一個有意思的點,由於 add 方法的入參是一個泛型,如下圖:

由於編譯器無法得知這裡需要 Father的兒子還是女兒,因此它不會接受任何型別的 Father。如果你先把 Son 向上轉型為 Father,也沒有關係——編譯器僅僅會拒絕呼叫像 add() 這樣引數列表中涉及萬用字元的方法。

但是!對於入參是 Object 型別的方法,比如 contains(Object o),編譯器允許呼叫他們。

下界萬用字元

下界萬用字元也很有意思,上界萬用字元其實已經指定了具體的型別,在下面的程式碼就是 Father,所以這個 list 可以隨意的 add 值。因為這裡 Son 和 Daughter 都是 Father 的子類,所以允許 add,但是 get 的時候就無法拿到具體值了。

public class Main {
    public static void main(String[] args) {
        List<? super Father> list = new ArrayList<>();
        list.add(new Son());
        list.add(new Daughter());
        for (Object o : list) {
            
        }
    }
}

泛型業界內有兩句總結:

  • 頻繁往外讀取內容的,適合用上界Extends。
  • 經常往裡插入的,適合用下界Super。

其他問題

在學習泛型的過程中,遇到一個很有意思的關於陣列的點也記錄一下。

如下例子(這個例子是 java 程式設計思想裡面的):

在 1 和 2 處竟然會丟擲異常!!我之前都不知道的,一直以為這樣能塞值成功。

這是因為,雖然定義的時候將 Apple 陣列向上轉型成了 Fruit 陣列,但是執行時的陣列機制知道它處理的是 Apple[],因此會在向陣列中放置異構型別時丟擲異常。

class Fruit {
}

class Apple extends Fruit {
}

class Jonathan extends Apple {
}

class Orange extends Fruit {
}

public class CovariantArrays {
    public static void main(String[] args) {
        Fruit[] fruit = new Apple[10];
        fruit[0] = new Apple(); // OK
        fruit[1] = new Jonathan(); // OK
        fruit[2] = new Orange(); // 1
        // Runtime type is Apple[], not Fruit[] or Orange[]:
        try {
            // Compiler allows you to add Fruit:
            fruit[0] = new Fruit(); // 2
        } catch (Exception e) {
            System.out.println(e);
        }
        try {
            // Compiler allows you to add Oranges:
            fruit[0] = new Orange(); // 3
        } catch (Exception e) {
            System.out.println(e);
        }
    }
}

文章為本人學習過程中的一些個人見解,漏洞是必不可少的,希望各位大佬多多指教,幫忙修復修復漏洞!