《一步到位》——完全理解Java泛型

一、什麼是泛型?

泛型,即“參數化類型”,意思爲將參數類型由原來的具體的類型,也設置成參數,即參數化(通常設置爲T)。

把類型明確的工作推遲到創建對象或調用方法的時候纔去明確的特殊的類型

Java泛型設計原則:只要在編譯時期沒有出現警告,那麼運行時期就不會出現ClassCastException異常.

注意:
  1. 參數化類型:把類型當作是參數一樣傳遞,<數據類型> 只能是引用類型
  2. 泛型只在編譯階段有效,編譯之後程序會採取去泛型化的措施,即在編譯過程中,正確檢驗泛型結果後,會將泛型的相關信息擦出,並且在對象進入和離開方法的邊界處添加類型檢查和類型轉換的方法。泛型信息不會進入到運行時階段。
  • ArrayList< E>中的E稱爲類型參數變量
  • ArrayList< Integer>中的Integer稱爲實際類型參數
  • 整個稱爲ArrayList< E>泛型類型
  • 整個ArrayList< Integer>稱爲參數化的類型ParameterizedType

二、爲什麼需要泛型

  1. 早期Java是使用Object來代表任意類型的,但是向下轉型有強轉的問題,這樣程序就不太安全。
  2. 假如沒有泛型,Collection、Map集合對元素的類型是沒有任何限制的。本來我的Collection集合裝載的是全部的Dog對象,但是外邊把Cat對象存儲到集合中,是沒有任何語法錯誤的。
  3. 把對象扔進集合中,集合是不知道元素的類型是什麼的,僅僅知道是Object。因此在get()的時候,返回的是Object。外邊獲取該對象,還需要強制轉換。
  • 向下轉型需要考慮安全性,如果父類引用的對象是父類本身,那麼在向下轉型的過程中是不安全的,編譯不會出錯,但是運行時會出現java.lang.ClassCastException錯誤,即子類不能直接指向父類對象
  • Java爲了解決不安全的向下轉型問題,引入泛型的概念。
  • 它可以使用instanceof來避免出錯此類錯誤即能否向下轉型,只有先經過向上轉型的對象才能繼續向下轉型。

使用增強for遍歷集合

	 //創建集合對象
        ArrayList<String> list = new ArrayList<>();

        list.add("hello");
        list.add("world");
        list.add("java");

        //遍歷,由於明確了類型.我們可以增強for
        for (String s : list) {
            System.out.println(s);
        }

三、泛型基礎(泛型類、泛型接口、泛型方法)

1、泛型類

泛型類就是把泛型定義在類上,用戶使用該類的時候,才把類型明確下來。因此,用戶明確了什麼類型,該類就代表着什麼類型,用戶在使用的時候就不用擔心強轉的問題,運行時轉換異常的問題了。

在類上定義的泛型,在類的方法中也可以使用。

此處T可以隨便寫爲任意標識,常見的如T、E、K、V等形式的參數常用於表示泛型
在實例化泛型類時,必須指定T的具體類型

public class ObjectTool<T> {
	// obj這個成員變量的類型爲T,T的類型由外部指定  
    private T obj;

    public T getObj() {
        return obj;
    }

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

用戶想要使用哪種類型,就在創建的時候指定類型。使用的時候,該類就會自動轉換成用戶想要使用的類型了。

	public static void main(String[] args) {
        //創建對象並指定元素類型,泛型的類型參數只能是類類型(包括自定義類),不能是簡單類型
        ObjectTool<String> strTool = new ObjectTool<>();
        strTool.setObject("testString");
        String s = strTool.getObject();
        System.out.println(s);

        // 如果我在這個對象裏傳入的是String類型的,它在編譯時期就通過不了了.
        ObjectTool<Integer> objectTool = new ObjectTool<>();
        objectTool.setObject(10);
        int i = objectTool.getObject();
        System.out.println(i);
    }
注意:定義的泛型類,就一定要傳入泛型類型實參麼?
  1. 在使用泛型的時候如果傳入泛型實參,則會根據傳入的泛型實參做相應的限制,此時泛型纔會起到本應起到的限制作用。
  2. 如果不傳入泛型類型實參的話,在泛型類中使用泛型的方法或成員變量定義的類型可以爲任何的類型。
  3. 泛型的類型參數只能是類類型,不能是簡單類型。

2、泛型接口

泛型接口與泛型類的定義及使用基本相同。

//定義一個泛型接口
public interface Generator<T> {
    public T next();
}

當實現泛型接口的類,未傳入泛型實參時:

未傳入泛型實參時,與泛型類的定義相同,在聲明類的時候,需將泛型的聲明也一起加到類中
如果不聲明泛型,如:class FruitGenerator implements Generator<T>,編譯器會報錯:"Unknown class"

class FruitGenerator<T> implements Generator<T>{
    @Override
    public T next() {
        return null;
    }
}

當實現泛型接口的類,傳入泛型實參時:

雖然我們只創建了一個泛型接口Generator<T>,但是我們可以爲T傳入實參,從而形成某種類型的Generator接口。
在實現類實現泛型接口時,如已將泛型類型傳入實參類型,則所有使用泛型的地方都會替換成傳入的實參類型

public class FruitGenerator implements Generator<String> {

    private String[] fruits = new String[]{"Apple", "Banana", "Pear"};

    @Override
    public String next() {
        Random rand = new Random();
        return fruits[rand.nextInt(3)];
    }
}

3、泛型方法

僅僅在某一個方法上需要使用泛型,外界僅僅是關心該方法,不關心類其他的屬性。泛型是先定義後使用的.
泛型方法,是在調用方法的時候指明泛型的具體類型 。

	 // 定義泛型方法
    public <Y> void show(Y y){
        System.out.println(y.getClass().getName()+":"+y);
    }

說明:

  1. public 與 返回值中間<T>非常重要,可以理解爲聲明此方法爲泛型方法。
  2. 只有聲明瞭<T>的方法纔是泛型方法,泛型類中的使用帶有泛型的成員方法並不是泛型方法。
  3. <T>表明該方法將使用泛型類型T,此時纔可以在方法中使用泛型類型T。
  4. 與泛型類的定義一樣,此處T可以隨便寫爲任意標識,常見的如T、E、K、V等形式的參數常用於表示泛型。

用戶傳遞進來的是什麼類型,返回值就是什麼類型了

		// 測試泛型方法
        ObjectTool tool = new ObjectTool();
        tool.show("hello");
        tool.show(12);

舉例子:

(1)基本用法
public class GenericTest {

    //這個類是個泛型類,在上面已經介紹過
    public class Generic<T>{
        private T key;

        public Generic(T key) {
            this.key = key;
        }

        /**
         * 雖然在方法中使用了泛型,但是這並不是一個泛型方法
         * 這只是類中一個普通的成員方法,只不過他的返回值是在聲明泛型類已經聲明過的泛型。
         * 所以在這個方法中才可以繼續使用 T 這個泛型。
         */
        public T getKey(){
            return key;
        }

        /**
         * 這個方法顯然是有問題的,在編譯器會給我們提示這樣的錯誤信息"cannot reslove symbol E"
         * 因爲在類的聲明中並未聲明泛型E,所以在使用E做形參和返回值類型時,編譯器會無法識別。
         */
//        public E setKey(E key){
//            return key;
//        }
    }

    /**
     * 這纔是一個真正的泛型方法。
     * 首先在public與返回值之間的<T>必不可少,這表明這是一個泛型方法,並且聲明瞭一個泛型T
     * 這個T可以出現在這個泛型方法的任意位置.
     * 泛型的數量也可以爲任意多個
     *      例如:public <T,K> K showKeyName(Generic<T> container){...}
     */
    public <T> T showKeyName(Generic<T> container){
        System.out.println("container key :" + container.getKey());
        //當然這個例子舉的不太合適,只是爲了說明泛型方法的特性。
        T test = container.getKey();
        return test;
    }

    // 這也不是一個泛型方法,這就是一個普通的方法,只是使用了Generic<Number>這個泛型類做形參而已。
    public void showKeyValue1(Generic<Number> obj){
        System.err.println("key value is " + obj.getKey());
    }

    /**
     * 這也不是一個泛型方法,這也是一個普通的方法,只不過使用了泛型通配符?
     * 同時這也印證了泛型通配符章節所描述的,?是一種類型實參,可以看做爲Number等所有類的父類
     */
    public void showKeyValue2(Generic<?> obj){
        System.err.println("key value is " + obj.getKey());
    }

    /**
     * 這個方法是有問題的,編譯器會爲我們提示錯誤信息:"UnKnown class 'E' "
     * 雖然我們聲明瞭<T>,也表明了這是一個可以處理泛型的類型的泛型方法。
     * 但是隻聲明瞭泛型類型T,並未聲明泛型類型E,因此編譯器並不知道該如何處理E這個類型。
     */
//    public <T> T showKeyName(Generic<E> container){
//        return null;
//    }

    /**
     * 這個方法也是有問題的,編譯器會爲我們提示錯誤信息:"UnKnown class 'T' "
     * 對於編譯器來說T這個類型並未項目中聲明過,因此編譯也不知道該如何編譯這個類。
     * 所以這也不是一個正確的泛型方法聲明。
     */
//    public void showkey(T genericObj){
//
//    }

    public static void main(String[] args) {
        GenericTest test = new GenericTest();

        Generic<String> generic = test.new Generic<>("**generic**");
        test.showKeyName(generic);

        Generic<Number> inte = test.new Generic<>(10);
        test.showKeyValue1(inte);

        Generic<?> sss = test.new Generic<>("++++++++");
        test.showKeyValue2(sss);
    }
}

結果:
在這裏插入圖片描述

(2)類中的泛型方法
public class GenericFruit {
    class Fruit{
        @Override
        public String toString() {
            return "fruit";
        }
    }

    class Apple extends Fruit{
        @Override
        public String toString() {
            return "apple";
        }
    }

    class Person{
        @Override
        public String toString() {
            return "Person";
        }
    }

    class GenerateTest<T>{
        public void show_1(T t){
            System.out.println(t.toString());
        }

        /**
         * 在泛型類中聲明瞭一個泛型方法,使用泛型E,這種泛型E可以爲任意類型。可以類型與T相同,也可以不同。
         * 由於泛型方法在聲明的時候會聲明泛型<E>,因此即使在泛型類中並未聲明泛型,編譯器也能夠正確識別泛型方法中識別的泛型。
         */
        public <E> void show_3(E t){
            System.out.println(t.toString());
        }

        // 在泛型類中聲明瞭一個泛型方法,使用泛型T,注意這個T是一種全新的類型,可以與泛型類中聲明的T不是同一種類型。
        public <T> void show_2(T t){
            System.out.println(t.toString());
        }

        public <T> void printMsg(T... args){
            for(T t : args){
                System.err.println("t is : " + t);
            }
        }
    }

    public static void main(String[] args) {
        GenericFruit generic = new GenericFruit();

        Apple apple = generic.new Apple();
        Person person = generic.new Person();
        GenerateTest<Fruit> generateTest = generic.new GenerateTest<Fruit>();

        // apple是Fruit的子類,所以這裏可以
        generateTest.show_1(apple);
        // 編譯器會報錯,因爲泛型類型實參指定的是Fruit,而傳入的實參類是Person
        //generateTest.show_1(person);

        //使用這兩個方法都可以成功
        generateTest.show_2(apple);
        generateTest.show_2(person);

        //使用這兩個方法也都可以成功
        generateTest.show_3(apple);
        generateTest.show_3(person);

        generateTest.printMsg("123",222,"++++","aaaa",55.36);
    }
}

結果:
在這裏插入圖片描述

(3)靜態方法與泛型

靜態方法有一種情況需要注意一下,那就是在類中的靜態方法使用泛型:靜態方法無法訪問類上定義的泛型;如果靜態方法操作的引用數據類型不確定的時候,必須要將泛型定義在方法上。

即:如果靜態方法要使用泛型的話,必須將靜態方法也定義成泛型方法

public class StaticGenerator<T> {

    /**
     * 如果在類中定義使用泛型的靜態方法,需要添加額外的泛型聲明(將這個方法定義成泛型方法)
     * 即使靜態方法要使用泛型類中已經聲明過的泛型也不可以。
     * 如:public static void show(T t){..},此時編譯器會提示錯誤信息:
          "StaticGenerator cannot be refrenced from static context"
     */
    public static <T> void show(T t){

    }
}
(4)泛型方法總結
  1. 如果能用泛型方法,那麼就儘量用泛型方法。
  2. 如果static方法要使用泛型能力,就必須使其成爲泛型方法。

4、泛型類派生出的子類

注意:泛型類是擁有泛型這個特性的類,它本質上還是一個Java類,那麼它就可以被繼承
繼承分兩種情況:

  1. 子類明確泛型類的類型參數變量
  2. 子類不明確泛型類的類型參數變量
子類明確泛型類的類型參數變量
// 把泛型定義在接口上
public interface Inter<T> {

    public abstract void show(T t);
}
// 子類明確泛型類的類型參數變量
public class InterImpl implements Inter<String> {

    @Override
    public void show(String s) {
        System.out.println(s);
    }
}

測試:

      Inter<String> i = new InterImpl();
      i.show("hello");
子類不明確泛型類的類型參數變量

當子類不明確泛型類的類型參數變量時,外界使用子類的時候,也需要傳遞類型參數變量進來,在實現類上需要定義出類型參數變量。

// 子類不明確泛型類的類型參數變量,實現類也要定義出<T>類型的
public class InterImpl<T> implements Inter<T> {

    @Override
    public void show(T t) {
        System.out.println(t);
    }
}
	Inter<String> ii = new InterImpl<>();
	ii.show("100");

注意:

  • 實現類的要是重寫父類的方法,返回值的類型是要和父類一樣的
  • 類上聲明的泛形只對非靜態成員有效

5、類型通配符

問題:方法接收一個集合參數,遍歷集合並把集合元素打印出來,怎麼辦?

// 不用泛型,只不過在編譯的時候會出現警告,說沒有確定集合元素的類型
public void test(List list){
    for(int i=0;i<list.size();i++){
        System.out.println(list.get(i));
    }
}

// 該方法只能遍歷裝載着Object的集合
public void test(List<Object> list){
	for(int i=0;i<list.size();i++){        
        System.out.println(list.get(i));
    }
}

// 使用類型通配符
public void test(List<?> list){
	for(int i=0;i<list.size();i++){ 
        System.out.println(list.get(i));
    }
}

泛型中的<Object>並不是像以前那樣有繼承關係的,也就是說List<Object>和List<String>是毫無關係的

?號通配符:表示可以匹配任意類型,任意的Java類都可以匹配。

注意:

  1. 當我們使用?號通配符的時候:就只能調對象與類型無關的方法,不能調用對象與類型有關的方法。因爲直到外界使用才知道具體的類型是什麼。即:List集合,是不能使用add()方法的。因爲add()方法是把對象丟進集合中,而現在我是不知道對象的類型是什麼。
  2. 類型通配符一般是使用'?'代替具體的類型實參,注意了,此處'?'是類型實參,而不是類型形參 。再直白點的意思就是,此處的'?'和Number、String、Integer一樣都是一種實際的類型,可以把?看成所有類型的父類。是一種真實的類型。
  3. 可以解決當具體類型不確定的時候,這個通配符就是?;當操作類型時,不需要使用類型的具體功能時,只使用Object類中的功能。那麼可以用?通配符來表未知類型。

設定通配符上限

對通配符做邊界限定,假如接收一個List集合,它只能操作數字類型的元素【Float、Integer、Double、Byte等】數字類型,怎麼做?
這時候需要用到設定通配符上限

List<? extends Number>

上述的含義是:List集合裝載的元素只能是Number的子類或自身
在這裏插入圖片描述

設定通配符下限

有通配符上限,那麼也應該有通配符下限

 <? super Type>

含義:傳遞進來的只能是Type或Type的父類
在這裏涉及到泛型的上限和下限中有一個原則:PECS(Producer Extends Consumer Super)

  • 如果要從集合中讀取類型T的數據,並且不能寫入,可以使用 ? extends 通配符;(Producer Extends)
  • 如果要從集合中寫入類型T的數據,並且不需要讀取,可以使用 ? super 通配符;(Consumer Super)
  • 如果既要存又要取,那麼就不要使用任何通配符。

參考:https://blog.csdn.net/xx326664162/article/details/52175283

6、通配符和泛型方法

大多時候,我們都可以使用泛型方法來代替通配符的。但是,我們使用通配符還是使用泛型方法呢?
使用通配符和泛型方法的原則:

  • 如果參數之間的類型有依賴關係,或者返回值是與參數之間有依賴關係的。那麼就使用泛型方法
  • 如果沒有依賴關係的,就使用通配符,通配符會靈活一些。

ObjectTool.java

public class ObjectTool<T> {

    private T object;

	public ObjectTool(T object){
        this.object = object;
    }

    public T getObject(){
        return object;
    }

    public void setObject(T object){
        this.object = object;
    }
}

測試類:

public class Main {

    public static void main(String[] args) {
        ObjectTool<String> generic1 = new ObjectTool<String>("11111");
        ObjectTool<Integer> generic2 = new ObjectTool<Integer>(2222);
        ObjectTool<Float> generic3 = new ObjectTool<Float>(2.4f);
        ObjectTool<Double> generic4 = new ObjectTool<Double>(2.56);

        //這一行代碼編譯器會提示錯誤,因爲String類型並不是Number類型的子類
        //showKeyValue1(generic1);

        showKeyValue1(generic2);
        showKeyValue1(generic3);
        showKeyValue1(generic4);
    }

    public static void showKeyValue1(ObjectTool<? extends Number> obj){
        System.err.println("key value is " + obj.getObject());
    }
}

再來一個泛型方法的例子:

	/**
     * 在泛型方法中添加上下邊界限制的時候,必須在權限聲明與返回值之間的<T>上添加上下邊界,即在泛型聲明的時候添加
     * public <T> T showKeyName(Generic<T extends Number> container),編譯器會報錯:"Unexpected bound"
     */
    public <T extends Number> T showKeyName(ObjectTool<T> container){
        System.out.println("container key :" + container.getObject());
        T test = container.getObject();
        return test;
    }

總結泛型的上下邊界添加,必須與泛型的聲明在一起

7、泛型擦除

JDK5提出了泛型這個概念,但是JDK5以前是沒有泛型的。也就是泛型是需要兼容JDK5以下的集合的。因此,當把帶有泛型特性的集合賦值給老版本的集合時候,會把泛型給擦除了。

注意:它保留的就類型參數的上限。

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

    //類型被擦除了,保留的是類型的上限,String的上限就是Object
    List list1 = list;

如果我把沒有類型參數的集合賦值給帶有類型參數的集合賦值,它也不會報錯,僅僅是提示未經檢查的轉換

	List list = new ArrayList();
    List<String> list2 = list;
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章