詳解Java泛型機制

我們做開發(fā)的時候一直會強調數(shù)據(jù)類型的概念,在Java中分為基本類型和引用數(shù)據(jù)類型,其中基本數(shù)據(jù)類型有八種,除了類以外,我們還可以使用接口繼承實現(xiàn)的方式來復用代碼,降低耦合度,提高開發(fā)的靈活性。而泛型則是將接口的概念進一步延伸,而泛型的意思就是廣泛的類型,無論是類、接口還是方法都可以應用于非常廣泛的類型,使得代碼和它們操作的數(shù)據(jù)類型不再需要綁定在一起,同一套代碼可以實現(xiàn)真正意義上的適用于多種數(shù)據(jù)類型,實現(xiàn)更靈活的代碼復用,并且能提高代碼的可讀性和安全性。說到這,可能你還會比較迷茫,接下來我們先看一個簡單的泛型,如下:

public class Pair<T> {
    T first;
    T second;
    public Pair(T first, T second){
        this.first = first;
        this.second = second;
    }
    public T getFirst() {
        return first;
    }
    public T getSecond() {
        return second;
    }
}

可以看出來Pair類就是一個泛型類,與普通的類區(qū)別在于:

1.類名后面多了一個<T>

2.參數(shù)first和second分別是泛型T類型

那么這個T是什么呢?T是一種泛指,表示類型參數(shù),泛型就是類型參數(shù)化,處理的數(shù)據(jù)不是固定的,而是可以動態(tài)指定類型作為參數(shù)傳入。那么定義的泛型類如何使用呢?如下:

Pair<Integer> minmax = new Pair<Integer>(1,100);
Integer min = minmax.getFirst();
Integer max = minmax.getSecond();

可以看到Pair<Integer>中的Integer就是之前定義的泛型T的實際類型參數(shù),當然這里的T可以是任何類型,我們這里可以指定為Integer,也可以指定為任何類型。同樣的,泛型的參數(shù)類型數(shù)量不是固定的,我們可以申明多個不同類型的動態(tài)泛型類型,兩個泛型之間使用逗號分割,如下:

public class Pair<U, V> {
    U first;
    V second;
    public Pair(U first, V second){
        this.first = first;
        this.second = second;
    }
    public U getFirst() {
        return first;
    }
    public V getSecond() {
        return second;
    }
}

改進后的Pair類可以這么使用:

Pair<String,Integer> pair = new Pair<String,Integer>("張三",100);

泛型的基本原理

看到上面的案例我們大概知道了一個簡單的泛型如何定義,那么不禁會有一個疑惑,那就是泛型類型到底是什么呢?我們?yōu)槭裁匆欢ㄒx一個類型參數(shù)呢?熟悉Java多態(tài)特性的我們都知道,我們完全可以定義一個通用的父類類型,然后傳遞具體的子類型不也能實現(xiàn)這樣的操作嗎?同樣的Java中也存在所有的類的基類--Object,如果我們直接使用Object不也可以嗎?如下:

public class Pair {
    Object first;
    Object second;
    public Pair(Object first, Object second){
        this.first = first;
        this.second = second;
    }
    public Object getFirst() {
        return first;
    }
    public Object getSecond() {
        return second;
    }
}

使用的時候的代碼只要這么改動:

Pair minmax = new Pair(1,100);
Integer min = (Integer)minmax.getFirst();//字段強制轉換
Integer max = (Integer)minmax.getSecond();//字段強制轉換

這樣使用其實是可以的,事實上Java提供的泛型機制其實底層就是如此實現(xiàn)的。之所以這么設計,與Java當初設計的時候的jvm虛擬機編譯機制有關系,要知道泛型設計的時候Java才到Jdk1.4版本,而我們都知道Java有編譯器和Java虛擬機,編譯器會幫我們把Java代碼轉換為.Class,虛擬機則是負責加載.Class,對于泛型類,Java編譯器會把泛型部分的代碼轉換為普通的代碼,即和上面的Object類型接管一樣,將類型的T進行擦除,替換為Object,并且進行必要的類型的強制轉換操作,所以在Java虛擬機執(zhí)行Java字節(jié)碼的過程中,其實和Object操作是一樣的,并不知道泛型,也不存在泛型。那么既然泛型還是會轉換為Object,進行泛型擦除,Java為什么要在1.5開始支持并設計出泛型機制呢?

泛型的好處

其實想要理解這點,我們不妨考慮一下,泛型的好處在哪?同時也去思考一下如果我們使用Object編程,缺陷會存在在哪?熟悉泛型的都知道,泛型有兩個好處:

1.更好的安全性

2.更好的可讀性

我們也知道Java語言在我們開發(fā)編譯的階段,ide就會進行代碼檢查,當我們的語法出現(xiàn)問題的時候,ide會在編譯階段就把錯誤標識出來,減少程序的潛在Bug數(shù)。但是我們不妨看下Object操作的代碼:

Pair pair = new Pair("張三",1);
Integer id = (Integer)pair.getFirst();
String name = (String)pair.getSecond();

可以看出來,無論id是否為Integer類型,或者name是否為String類型,我們在編譯階段,由于類型為Object,我們都會進行強制轉換操作,在編譯期這些操作都是語法合理的,并不會報錯,但是如果這些字段中存在類型錯誤,也必須等到程序運行到這里才會提示ClassCastException異常,但是如果我們使用的是泛型機制,并且使用的時候標明了類型為String和Integer,那么如果我們使用的類型不一致,在編譯時已經(jīng)報錯,必須修改后才可以成功運行,如下:

Pair<String,Integer> pair = new Pair<>("張三",1);
Integer id = pair.getFirst(); //編譯錯誤
String name = pair.getSecond(); //編譯錯誤

所以很明顯的可以看出來,如果使用了泛型后,類的后綴添加對應的泛型類型,我們很明確的知道具體的類型是什么,提高開發(fā)的可讀性,并且因為ide會做類型檢查,所以安全性也會更高

泛型方法

當然泛型的作用域范圍比較廣,我們不僅可以定義在類/接口的申明上,我們也可以將泛型作用在方法上,與類的泛型相互隔離,實現(xiàn)更精細粒度的泛型操作。并且需要注意的是,一個類的泛型定義和方法的泛型定義并無直接關系,兩者是相互獨立的,即類的泛型可以定義為T,而方法也可以定義為泛型T,但是這兩個T并不屬于同一個。首先我們先看一個泛型方法的案例:

public static <T> int indexOf(T[] arr, T elm){
    for(int i=0; i<arr.length; i++){
            if(arr[i].equals(elm)){
            return i;
        }
    }
    return -1;
}

可以看出來,indexOf方法就是一個泛型方法,使用的時候,我們可以如下:

indexOf(new Integer[]{1,3,5}, 10)

同樣的泛型方法擁有和泛型類一樣的所有特性,也可以定義多個泛型參數(shù)在方法上,比如:

public static <U,V> Pair<U,V> createPair(U first, V second){
    Pair<U,V> pair = new Pair<>(first, second);
    return pair;
}

但是與泛型類不同的是,使用的時候只需要傳入確定類型的值即可,并不需要申明泛型類型后綴,如下:

createPair("張三",1);

泛型的上限界定

在前面的學習中我們都知道泛型擦除會轉化為Object類型,但是我們能不能給Object的范圍縮小呢?即限制泛型的父類類型上限是多少,在Java中其實是支持的,而泛型中支持這個上限界定是使用了extends關鍵字來表示的,當然這里的父類類型可以是接口、類或者類型參數(shù),我們分別介紹下:

接口作為父類類型

比如我們開發(fā)中遇到一個場景,我們必須實現(xiàn)Comparable接口來實現(xiàn)動態(tài)的類型的比較,這個時候代碼如下:

public static <T extends Comparable> T max(T[] arr){
    T max = arr[0];
    for(int i=1; i<arr.length; i++){
            if(arr[i].compareTo(max)>0){
            max = arr[i];
        }
    }
    return max;
}

max是泛型類型T的數(shù)組的對應下標的值,不過這么編寫代碼的話,會被編譯器警告,因為Comparable接口本身也是個泛型接口,所以我們寫的時候建議也去指定Comparable接口的泛型上界,修改如下:

public static <T extends Comparable<T>> T max(T[] arr){
...................
}

此種方式可以實現(xiàn)泛型類型的遞歸類型限制傳遞

上界為具體類

還記得我們上面的實例Pair類使用的泛型類型,我們可以實現(xiàn)一個子類:

public class NumberPair<U extends Number, V extends Number> extends Pair<U, V> {
    public NumberPair(U first, V second) {
        super(first, second);
    }
}   

當我們限制了對應的類型范圍后,我們就可以把first和second變量作為Number類型進行處理了,比如我們內部有一個求和的方法:

public double plus(){
    return getFirst().doubleValue() + getSecond().doubleValue();
}

所以當我們定義完后,我們的使用即為如下這樣:

NumberPair<Integer, Double> pair = new NumberPair<>(10, 12.34);
double sum = pair.plus();

可以看出來,限制了泛型類型范圍后,編譯器檢查的會更嚴格,如果類型不對直接會報錯,并且泛型擦除的時候轉換的類型則為指定的范圍上界的類型

泛型的通配符

上面我們提到了一些例子,就是使用了參數(shù)類型作為范圍上界,但是這種寫法比較繁瑣,有木有更簡化的寫法呢?當然有,泛型支持通配符形式,可以簡化范圍上界的泛型寫法,一個簡單的通配符泛型如下:

public void addAll(DynamicArray<? extends E> c) {
    for(int i=0; i<c.size; i++){
        add(c.get(i));
    }
}

可以看到當前的寫法中c的類型是DynamicArray<? extends E>類型,?表示通配符,<?extends E>表示有限定通配符,具體需要匹配泛型E或者E的子類型即可,至于具體是什么類型,完全可以未知,當我們使用的時候,代碼如下:

DynamicArray<Number> numbers = new DynamicArray<>();
DynamicArray<Integer> ints = new DynamicArray<>();
ints.add(100);
ints.add(34);
numbers.addAll(ints);

這里E是Number類型的時候,?可以匹配為DynamicArray<Integer>,那么通配符和范圍上界指定的效果一樣,這兩者有什么區(qū)別呢?

1.<T extends E>寫法僅限于用于定義類型參數(shù),申明了一個類型參數(shù)T(使用的時候必須指定泛型類型)

2.<? extends E>用于實例化類型參數(shù),可以用于實例化泛型變量中的類型參數(shù),只是當前類型可以是未知的,只需要知道范圍上限,即屬于泛型E的子類即可(使用的時候可以不指定泛型類型,或者直接傳遞子類類型即可)

那么我們什么時候使用通配符,什么時候需要定義類型參數(shù)范圍呢?首先我們先來認知下通配符分類以及各類通配符的用法

無限定通配符

在泛型中,除了上述的有限定通配符以外,還有無限定通配符超類型通配符,我們首先來了解無限定通配符,使用無限定通配符實現(xiàn)一個簡單的DynamicArray中查找元素,代碼如下:

public static int indexOf(DynamicArray<?> arr, Object elm){
    for(int i=0; i<arr.size(); i++){
        if(arr.get(i).equals(elm)){
            return i;
        }
    }
    return -1;
}

刻意看到上述的泛型即使用了無限定通配符,當然此通配符也可以使用泛型類型T來代替,效果是相同的,不過無限定通配符使用起來更簡潔,當然無論是上述的哪一種通配符,都有一個限制--只能讀,不可以寫入,我們先看例子:

DynamicArray<Integer> ints = new DynamicArray<>();
DynamicArray<? extends Number> numbers = ints;
Integer a = 200;
numbers.add(a); //代碼錯誤,不允許添加
numbers.add((Number)a); //代碼錯誤,不允許添加
numbers.add((Object)a); //代碼錯誤,不允許添加

可以看到這三種方法,都嘗試在泛型類型未確定的時候嘗試插入操作,無一例外都失敗了,這里就是Java對與泛型的類型檢查的優(yōu)化,無論是?通配符,還是<? extends E>方式的泛型,這里的泛型類型都是不確定的,所以允許插入后就會有類型安全的問題。當然除了這一點以外,如果返回值依賴某個引用類型參數(shù),也不允許使用通配符,如下:

public static <T extends Comparable<T>> T max(DynamicArray<T> arr){
    T max = arr.get(0);
        for(int i=1; i<arr.size(); i++){
            if(arr.get(i).compareTo(max)>0){
                max = arr.get(i);
            }
        }
    return max;
}

這里的代碼如果使用通配符,就會出現(xiàn)意想不到的問題,所以也無法使用通配符操作,從上面我么可以總結出,無限定通配符和泛型類型參數(shù)的關系,如下:

1.無限定通配符能修飾的泛型,都可以使用泛型類型參數(shù)的方式替換

2.通配符可以減少泛型類型參數(shù),代碼更簡潔,可讀性更好

3.如果類型參數(shù)之間有依賴關系,或者返回值依賴于傳遞的類型參數(shù),這里只能使用泛型類型參數(shù)

超類型通配符

上面我們知道可以在泛型中存在繼承關系,所以我們可以指定泛型的父類上界,也可以使用有限定通配符,一定程度上可以實現(xiàn)我們開發(fā)的靈活簡化,但是也存在這樣的場景,比如我們知道某一個具體的子類實現(xiàn),但是我們希望無論是哪一級的父類型都可以作為通用的操作,這個時候我們不確定類型的就是超類了,還能使用泛型嗎?答案是能,泛型在Java1.6中加入了超類型通配符操作,形式為<? super E>,首先我們先看沒有超類型通配符的一個簡單場景,如下:

//定義了一個copy方法
public void copyTo(DynamicArray<E> dest){
    for(int i=0; i<size; i++){
        dest.add(get(i));
    }
}   

我們想要做的操作很簡單,只要將當前容器的元素傳遞如對應的容器中,這個時候我們可能希望這么使用:

DynamicArray<Integer> ints = new DynamicArray<Integer>();
ints.add(100);
ints.add(34);
//構建一個Number父類型的動態(tài)數(shù)組
DynamicArray<Number> numbers = new DynamicArray<Number>();
ints.copyTo(numbers);

按照java特性來說,Integer是Number的子類型,將Integer的實例數(shù)組對象copy進入父類型的數(shù)組中是完全合理的,但是由于這里使用了泛型,指定的類型參數(shù)不一致,導致java編譯器會提示編譯錯誤,但是我們使用了超類型通配符以后,問題迎仍而解,如下:

public void copyTo(DynamicArray<? super E> dest){
    for(int i=0; i<size; i++){
        dest.add(get(i));
    }
}

通配符比較

現(xiàn)在我們對三種通配符都有了一定的了解了,將三種通配符進行比較和總結如下:

1.三種通配符存在的意義都是為了使得java動態(tài)代碼更加靈活,可以接受更廣泛的類型

2.<? super E>通配符方式更適合靈活寫入的場景,使得java編譯器不會捕捉子類型寫入父類型的容器的錯誤,并且不能被泛型參數(shù)類型的方式替換

3.<?>和<? extends E>方式更適合用于靈活的讀取,使得代碼可以讀取E和任何子類的對象,這里的通配符操作和泛型類型參數(shù)操作完全等同,可以互相替換

泛型使用的細節(jié)與注意點

學習到這里,可能了解了泛型的原理,其實就是通過java的類型擦除的特性實現(xiàn)的,實際編譯的時候還是會轉換為Object類型或者限定的父類型,但是使用泛型并不是任何場景都適用的,下面我們來羅列一下泛型的細節(jié)與使用的局限性:

使用泛型類、接口和泛型方法時

需要注意:

1.基本類型不能作為實例化類型參數(shù),應使用其包裝類型

2.運行時類型信息不適用于泛型,例如"string".class這種不被允許作為泛型實例傳遞

3.類型擦除也可能會出現(xiàn)一些沖突,例如父類型實現(xiàn)某個接口,父類型作為泛型的時候,子類如果想實現(xiàn)父類接口的某個方法,重新實現(xiàn)該接口就會出現(xiàn)錯誤

定義泛型類、接口和泛型方法時

也需要注意:

1.不能通過類型參數(shù)創(chuàng)建對象,例如:T t = new T(),如果真的需要創(chuàng)建對象,建議使用Class類型作為類型參數(shù)而非泛型

2.泛型類的類型不能被用作靜態(tài)變量或者靜態(tài)方法

?著作權歸作者所有,轉載或內容合作請聯(lián)系作者
【社區(qū)內容提示】社區(qū)部分內容疑似由AI輔助生成,瀏覽時請結合常識與多方信息審慎甄別。
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發(fā)布,文章內容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

相關閱讀更多精彩內容

  • “泛型”這個術語的意思是:"適用于許多許多的類型”。如何做到這一點呢,正是通過解耦類或方法與所使用的類型之間的約束...
    王偵閱讀 1,348評論 0 0
  • 本系列文章將整理到我在GitHub上的《Java面試指南》倉庫,更多精彩內容請到我的倉庫里查看 https://g...
    程序員黃小斜閱讀 684評論 0 0
  • 最近項目組在進行泛型代碼編寫時遇到很多困難,討論下來發(fā)現(xiàn)大家對這個概念都是一知片解,然而在我們的項目開發(fā)過程中,又...
    Caprin閱讀 4,969評論 2 47
  • 自從凌冰答應了夏晨就私教,夏晨在公司就干脆做起了甩手掌柜,天天掐著點過來陪做功課,其實也不算是做功課,凌冰本身成績...
    sugax閱讀 362評論 0 1
  • 最經(jīng)在經(jīng)營過程中,感覺到有些疲憊-戰(zhàn)略戰(zhàn)術皆思考,正應“大染坊“陳六子說,富貴險中求,商業(yè)原本就是無限的戰(zhàn)爭游戲 ...
    紙上芭蕾閱讀 151評論 1 1

友情鏈接更多精彩內容