java泛型原理及其使用


一、什么是泛型

  Java從1.5之后支持泛型,泛型的本質是類型參數,也就是說所操作的數據類型被指定為一個參數。這種參數類型可以用在類、接口和方法的創建中,分別稱為泛型類、泛型接口、泛型方法。

  若不支持泛型,則表現為支持Object,不是特定的泛型。泛型是對 Java 語言的類型系統的一種擴展,以支持創建可以按類型進行參數化的類。可以把類型參數看作是使用參數化類型時指定的類型的一個占位符,就像方法的形式參數是運行時傳遞的值的占位符一樣。許多重要的類,比如集合框架,都已經成為泛型化的了。

二、泛型有什么優點

  泛型的好處是在編譯的時候檢查類型安全,並且所有的強制轉換都是自動和隱式的,以提高代碼的重用率。

  1、類型安全

  泛型的主要目標是提高 Java 程序的類型安全。通過知道使用泛型定義的變量的類型限制,編譯器可以在一個高得多的程度上驗證類型假設。沒有泛型,這些假設就無法落實到代碼中,僅僅能停留在設計方案或者注釋中。 

  2、消除強制類型轉換

  泛型的一個附帶好處是,消除源代碼中的許多強制類型轉換。這使得代碼更加可讀,並且減少了強制轉換代碼和出錯機會。

  3、潛在的性能收益

  泛型為較大的優化帶來可能。在泛型的初始實現中,編譯器將強制類型轉換(沒有泛型的話,程序員會指定這些強制類型轉換)插入生成的字節碼中。

三、泛型如何表示

  我們在泛型中是用的T,E,K,V有什么區別呢,實際上使用大寫字母A,B,C,D......X,Y,Z定義的,就都是泛型,把T換成A也一樣,這里T只是名字上的意義而已,如:

  •  表示不確定的java類型,類型是未知的。
  • T (type) 表示具體的一個java類型,如果要定義超過兩個,三個或三個以上的泛型參數可以使用T1, T2, ..., Tn
  • K V (key value) 分別代表java鍵值中的Key Value
  • E (element) 代表Element
  • extends、super 泛型的參數類型可以使用extends、super語句,例如<T extends superclass>。習慣上稱為“有界類型”。

四、泛型的原理

  泛型是一種語法糖,泛型這種語法糖的基本原理是類型擦除,即編譯器會在編譯期間「擦除」泛型語法並相應的做出一些類型轉換動作。例如:

public class Caculate<T> {
    private T num;
}

  我們定義了一個泛型類,定義了一個屬性成員,該成員的類型是一個泛型類型,這個 T 具體是什么類型,我們也不知道,它只是用於限定類型的。反編譯一下這個 Caculate 類:

public class Caculate{
    public Caculate(){}
    private Object num;
}

  發現編譯器擦除 Caculate 類后面的兩個尖括號,並且將 num 的類型定義為 Object 類型。

  那么是不是所有的泛型類型都以 Object 進行擦除呢?大部分情況下,泛型類型都會以 Object 進行替換,而有一種情況則不是。那就是使用到了extends和super語法的有界類型,如:

public class Caculate<T extends String> {
    private T num;
}

  這種情況的泛型類型,num 會被替換為 String 而不再是 Object。這是一個類型限定的語法,它限定 T 是 String 或者 String 的子類,也就是你構建 Caculate 實例的時候只能限定 T 為 String 或者 String 的子類,所以無論你限定 T 為什么類型,String 都是父類,不會出現類型不匹配的問題,於是可以使用 String 進行類型擦除。

  實際上編譯器會正常的將使用泛型的地方編譯並進行類型擦除,然后返回實例。但是除此之外的是,如果構建泛型實例時使用了泛型語法,那么編譯器將標記該實例並關注該實例后續所有方法的調用,每次調用前都進行安全檢查,非指定類型的方法都不能調用成功。

  實際上編譯器不僅關注一個泛型方法的調用,它還會為某些返回值為限定的泛型類型的方法進行強制類型轉換,由於類型擦除,返回值為泛型類型的方法都會擦除成 Object 類型,當這些方法被調用后,編譯器會額外插入一行 checkcast 指令用於強制類型轉換。這一個過程就叫做『泛型翻譯』。

五、泛型使用

  泛型類型可以用在類、接口和方法的創建中,分別稱為泛型類、泛型接口、泛型方法,其中類和接口使用方式大致一致。

  1、泛型類和接口

訪問修飾符 class/interface 類名或接口名<限定類型變量名>

  如泛型類和接口:

//此處T可以隨便寫為任意標識,常見的如T、E、K、V等形式的參數常用於表示泛型
//在實例化泛型類時,必須指定T的具體類型
public class Generic<T>{ 
    //key這個成員變量的類型為T,T的類型由外部指定  
    private T key;

    public Generic(T key) { //泛型構造方法形參key的類型也為T,T的類型由外部指定
        this.key = key;
    }

    public T getKey(){ //泛型方法getKey的返回值類型為T,T的類型由外部指定
        return key;
    }
}


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

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

/**
 * 未傳入泛型實參時,與泛型類的定義相同,在聲明類的時候,需將泛型的聲明也一起加到類中
 * 即:class FruitGenerator<T> implements Generator<T>{
 * 如果不聲明泛型,如:class FruitGenerator implements Generator<T>,編譯器會報錯:"Unknown class"
 */
class FruitGenerator<T> implements Generator<T>{
    @Override
    public T next() {
        return null;
    }
}

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

/**
 * 傳入泛型實參時:
 * 定義一個生產器實現這個接口,雖然我們只創建了一個泛型接口Generator<T>
 * 但是我們可以為T傳入無數個實參,形成無數種類型的Generator接口。
 * 在實現類實現泛型接口時,如已將泛型類型傳入實參類型,則所有使用泛型的地方都要替換成傳入的實參類型
 * 即:Generator<T>,public T next();中的的T都要替換成傳入的String類型。
 */
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)];
    }
}

  2、泛型方法

  泛型類/接口,是在實例化類的時候指明泛型的具體類型;泛型方法,是在調用方法的時候指明泛型的具體類型 。

  方法並不一定依賴其外部的類或者接口,它可以獨立存在,也可以依賴外圍類存在。

public E get(int index) {
    rangeCheck(index);
    return elementData(index);
}

  ArrayList 的這個 get 方法就是一個泛型方法,它依賴外圍 ArrayList 聲明的 E 這個泛型類型,也就是它沒有自己聲明一個泛型類型而用的外圍類的。當然,另一種方式就是自己申明一個泛型類型並使用:

/**
 * 泛型方法的基本介紹
 * @param tClass 傳入的泛型實參
 * @return T 返回值為T類型
 * 說明:
 *     1)public 與 返回值中間<T>非常重要,可以理解為聲明此方法為泛型方法。
 *     2)只有聲明了<T>的方法才是泛型方法,泛型類中的使用了泛型的成員方法並不是泛型方法。
 *     3)<T>表明該方法將使用泛型類型T,此時才可以在方法中使用泛型類型T。
 *     4)與泛型類的定義一樣,此處T可以隨便寫為任意標識,常見的如T、E、K、V等形式的參數常用於表示泛型。
 */
public <T> T genericMethod(Class<T> tClass)throws InstantiationException ,
  IllegalAccessException{
        T instance = tClass.newInstance();
        return instance;
}

  如果既不依賴外圍又未自己聲明的話會:

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

  3、通配符

  通配符是用於解決泛型之間引用傳遞問題的特殊語法。如:

public static void main(String[] args){
    Integer[] integerArr = new Integer[2];
    Number[] numberArr = new Number[2];
    numberArr = integerArr;

    ArrayList<Integer> integers = new ArrayList<>();
    ArrayList<Number> numbers = new ArrayList<>();
    numbers = integers;//編譯不通過
}

  Java 中,數組是協變的,即 Integer extends Number,那么子類數組實例是可以賦值給父類數組實例的。那是由於 Java 中的數組類型本質上會由虛擬機運行時動態生成一個類型,這個類型除了記錄數組的必要屬性,如長度,元素類型等,會有一個指針指向內存某個位置,這個位置就是該數組元素的起始位置。

  所以子類數組實例賦值父類數組實例,只不過意味着父類數組實例的引用指向堆中子類數組而已,並不會有所沖突,因此是 Java 允許這種操作的。而泛型是不允許這么做的,為什么呢?我們假設泛型允許這種協變,看看會有什么問題。

ArrayList<Integer> integers = new ArrayList<>();
ArrayList<Number> numbers = new ArrayList<>();
numbers = integers;//假設的前提下,編譯器是能通過的
numbers.add(23.5);

  假設 Java 允許泛型協變,那么上述代碼在編譯器看來是沒問題的,但運行時就會出現問題。這個 add 方法實際上就將一個浮點數放入了整型容器中了,雖然由於類型擦除並不會對程序運行造成問題,但顯然違背了泛型的設計初衷,容易造成邏輯混亂,所以 Java 干脆禁止泛型協變。

  所以雖然 ArrayList<Integer> 和 ArrayList<Number>編譯器類型擦除之后都是 ArrayList 的實例,但是起碼在編譯器看來,這兩者是兩種不同的類型。那么,假如有某種需求,我們的方法既要支持子類泛型作為形參傳入,也要支持父類泛型作為形參傳入,又該怎么辦呢?

  我們使用通配符處理這樣的需求,例如:

public void test1(ArrayList<? extends Number> list){}
// 或者
public void test2(自定義類<?> obj){}

  如上ArrayList<? extends Number> 表示泛型類型具體是什么不知道,但是具體類型必須是 Number 及其子類類型。例如:ArrayList<Number>,ArrayList<Integer>,ArrayList<Double> 等。

  但是,通配符往往用於方法的形參中,而不允許用於定義和調用語法中。因為? 代表不確定類型,通配符匹配出來的泛型類型只能讀取,不能寫。所以你只能讀取里面的數據,不能瞎往里面添加元素。如:

ArrayList<Number> list = new ArrayList<>();
ArrayList<?> arrayList = list;
// 后面的會編譯報錯
arrayList.add(32);
arrayList.add("fadsf");
arrayList.add(new Object());

  4、類型限定

  在使用泛型的時候,我們還可以為傳入的泛型類型實參進行上下邊界的限制,如:類型實參只准傳入某種類型的父類或某種類型的子類。為泛型添加上邊界,即傳入的類型實參必須是指定類型的子類型。如:<? super T> 與 <? extends T>、<T extends String>

  如一個類:

public class Generic<T>{
    private T key;
    public Generic(T key) {
        this.key = key;
    }
    public T getKey(){
        return key;
    }
    public void showKeyValue1(Generic<? extends Number> obj){
        Log.d("泛型測試","key value is " + obj.getKey());
    }
}

  那么:

Generic<String> generic1 = new Generic<String>("11111");
Generic<Integer> generic2 = new Generic<Integer>(2222);
Generic<Float> generic3 = new Generic<Float>(2.4f);
Generic<Double> generic4 = new Generic<Double>(2.56);

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

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

  如果改為:

public class Generic<T extends Number>{
    private T key;
    public Generic(T key) {
        this.key = key;
    }
    public T getKey(){
        return key;
    }
    public void showKeyValue1(Generic<? extends Number> obj){
        Log.d("泛型測試","key value is " + obj.getKey());
    }
}
//這一行代碼也會報錯,因為String不是Number的子類
Generic<String> generic1 = new Generic<String>("11111");

  在泛型方法中添加上下邊界限制的時候,必須在權限聲明與返回值之間的<T>上添加上下邊界,即在泛型聲明的時候添加:

//public <T> T showKeyName(Generic<T extends Number> container),編譯器會報錯:"Unexpected bound"
public <T extends Number> T showKeyName(Generic<T> container){
    System.out.println("container key :" + container.getKey());
    T test = container.getKey();
    return test;
}

六、泛型限制

  1、靜態屬性不支持泛型,如:

private static T target;    //編譯報錯

  2、在類中的靜態方法無法訪問類上定義的泛型

  如果靜態方法操作的引用數據類型不確定的時候,必須要將泛型定義在方法上。即:如果靜態方法要使用泛型的話,必須將靜態方法也定義成泛型方法 。

public class StaticGenerator<T> {

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

  3、泛型的類型參數只能是類類型(包括自定義類),不能是簡單類型,可使用其包裝類型代替(如:Integer)。

  4、不能使用泛型類異常

  5、不能實例化泛型對象:如:T t = new T();

  6、不能實例化泛型數組

  經過查看sun的說明文檔,在java中是”不能創建一個確切的泛型類型的數組”的。

  也就是說下面的這個例子是不可以的:

List<String>[] ls = new ArrayList<String>[10];  

  而使用通配符創建泛型數組是可以的,如下面這個例子:

List<?>[] ls = new ArrayList<?>[10]; 

  這樣也是可以的:

List<String>[] ls = new ArrayList[10];

  7、一個泛型類被其所有調用共享

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

  會返回true,因為一個泛型類的所有實例在運行時具有相同的運行時類(class),而不管他們的實際類型參數。事實上,泛型之所以叫泛型,就是因為它對所有其可能的類型參數,有同樣的行為;同樣的類可以被當作許多不同的類型。作為一個結果,類的靜態變量和方法也在所有的實例間共享。這就是為什么在靜態方法或靜態初始化代碼中或者在靜態變量的聲明和初始化時使用類型參數(類型參數是屬於具體實例的)是不合法的原因。

  8、不能對確切的泛型類型使用instanceof操作

  泛型類被所有其實例(instances)共享的另一個暗示是檢查一個實例是不是一個特定類型的泛型類是沒有意義的。如:

Collection cs = new ArrayList<String>();
if (cs instanceof Collection<String>) { ...} // 非法


免責聲明!

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



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