通俗易懂講泛型


由於博客園的 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 方法的入參是一個泛型,如下圖:img

由於編譯器無法得知這里需要 Father的兒子還是女兒,因此它不會接受任何類型的 Father。如果你先把 Son 向上轉型為 Father,也沒有關系——編譯器僅僅會拒絕調用像 add() 這樣參數列表中涉及通配符的方法。

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

img

下界通配符

下界通配符也很有意思,上界通配符其實已經指定了具體的類型,在下面的代碼就是 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);
        }
    }
}

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


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM