Java泛型

我們知道,使用變量之前要定義,定義一個變量時必須要指明它的數據類型,什么樣的數據類型賦給什么樣的值。

假如我們現在要定義一個類來表示坐標,要求坐標的數據類型可以是整數、小數和字符串,例如:
x = 10、y = 10
x = 12.88、y = 129.65
x = "東京180度"、y = "北緯210度"

針對不同的數據類型,除了借助方法重載,還可以借助自動裝箱和向上轉型。我們知道,基本數據類型可以自動裝箱,被轉換成對應的包裝類;Object 是所有類的祖先類,任何一個類的實例都可以向上轉型為 Object 類型,例如:
int --> Integer --> Object
double -->Double --> Object
String --> Object

這樣,只需要定義一個方法,就可以接收所有類型的數據。請看下面的代碼:

public class Demo {
    public static void main(String[] args){
        Point p = new Point();
        p.setX(10);  // int -> Integer -> Object
        p.setY(20);
        int x = (Integer)p.getX();  // 必須向下轉型
        int y = (Integer)p.getY();
        System.out.println("This point is:" + x + ", " + y);
       
        p.setX(25.4);  // double -> Integer -> Object
        p.setY("東京180度");
        double m = (Double)p.getX();  // 必須向下轉型
        double n = (Double)p.getY();  // 運行期間拋出異常
        System.out.println("This point is:" + m + ", " + n);
    }
}
class Point{
    Object x = 0;
    Object y = 0;
    public Object getX() {
        return x;
    }
    public void setX(Object x) {
        this.x = x;
    }
    public Object getY() {
        return y;
    }
    public void setY(Object y) {
        this.y = y;
    }
}

上面的代碼中,生成坐標時不會有任何問題,但是取出坐標時,要向下轉型,向下轉型存在著風險,而且編譯期間不容易發現,只有在運行期間才會拋出異常,所以要盡量避免使用向下轉型。運行上面的代碼,第12行會拋出 java.lang.ClassCastException 異常。那么,有沒有更好的辦法,既可以不使用重載(有重復代碼),又能把風險降到最低呢?有,可以使用泛型類(Java Class),它可以接受任意類型的數據。所謂“泛型”,就是“寬泛的數據類型”,任意的數據類型。更改上面的代碼,使用泛型類:

public class Demo {
    public static void main(String[] args){
        // 實例化泛型類
        Point<Integer, Integer> p1 = new Point<Integer, Integer>();
        p1.setX(10);
        p1.setY(20);
        int x = p1.getX();
        int y = p1.getY();
        System.out.println("This point is:" + x + ", " + y);
       
        Point<Double, String> p2 = new Point<Double, String>();
        p2.setX(25.4);
        p2.setY("東京180度");
        double m = p2.getX();
        String n = p2.getY();
        System.out.println("This point is:" + m + ", " + n);
    }
}

// 定義泛型類
class Point<T1, T2>{
    T1 x;
    T2 y;
    public T1 getX() {
        return x;
    }
    public void setX(T1 x) {
        this.x = x;
    }
    public T2 getY() {
        return y;
    }
    public void setY(T2 y) {
        this.y = y;
    }
}

運行結果:
This point is:10, 20
This point is:25.4, 東京180度

與普通類的定義相比,上面的代碼在類名后面多出了 <T1, T2>,T1, T2 是自定義的標識符,也是參數,用來傳遞數據的類型,而不是數據的值,我們稱之為類型參數。在泛型中,不但數據的值可以通過參數傳遞,數據的類型也可以通過參數傳遞。T1, T2 只是數據類型的占位符,運行時會被替換為真正的數據類型。

傳值參數(我們通常所說的參數)由小括號包圍,如 (int x, double y),類型參數(泛型參數)由尖括號包圍,多個參數由逗號分隔,如 <T> 或 <T, E>。

類型參數需要在類名后面給出。一旦給出了類型參數,就可以在類中使用了。類型參數必須是一個合法的標識符,習慣上使用單個大寫字母,通常情況下,K 表示鍵,V 表示值,E 表示異常或錯誤,T 表示一般意義上的數據類型。

泛型類在實例化時必須指出具體的類型,也就是向類型參數傳值,格式為:
className variable<dataType1, dataType2> = new className<dataType1, dataType2>();
也可以省略等號右邊的數據類型,但是會產生警告,即:
className variable<dataType1, dataType2> = new className();

因為在使用泛型類時指明了數據類型,賦給其他類型的值會拋出異常,既不需要向下轉型,也沒有潛在的風險,比本文一開始介紹的自動裝箱和向上轉型要更加實用。

注意:
泛型是 Java 1.5 的新增特性,它以C++模板為參照,本質是參數化類型(Parameterized Type)的應用。
類型參數只能用來表示引用類型,不能用來表示基本類型,如 int、double、char 等。但是傳遞基本類型不會報錯,因為它們會自動裝箱成對應的包裝類。

泛型方法

除了定義泛型類,還可以定義泛型方法,例如,定義一個打印坐標的泛型方法:

public class Demo {
    public static void main(String[] args){
        // 實例化泛型類
        Point<Integer, Integer> p1 = new Point<Integer, Integer>();
        p1.setX(10);
        p1.setY(20);
        p1.printPoint(p1.getX(), p1.getY());
       
        Point<Double, String> p2 = new Point<Double, String>();
        p2.setX(25.4);
        p2.setY("東京180度");
        p2.printPoint(p2.getX(), p2.getY());
    }
}

// 定義泛型類
class Point<T1, T2>{
    T1 x;
    T2 y;
    public T1 getX() {
        return x;
    }
    public void setX(T1 x) {
        this.x = x;
    }
    public T2 getY() {
        return y;
    }
    public void setY(T2 y) {
        this.y = y;
    }
   
    // 定義泛型方法
    public <T1, T2> void printPoint(T1 x, T2 y){
        T1 m = x;
        T2 n = y;
        System.out.println("This point is:" + m + ", " + n);
    }
}

運行結果:
This point is:10, 20
This point is:25.4, 東京180度

上面的代碼中定義了一個泛型方法 printPoint(),既有普通參數,也有類型參數,類型參數需要放在修飾符后面、返回值類型前面。一旦定義了類型參數,就可以在參數列表、方法體和返回值類型中使用了。

與使用泛型類不同,使用泛型方法時不必指明參數類型,編譯器會根據傳遞的參數自動查找出具體的類型。泛型方法除了定義不同,調用就像普通方法一樣。

注意:泛型方法與泛型類沒有必然的聯系,泛型方法有自己的類型參數,在普通類中也可以定義泛型方法。泛型方法 printPoint() 中的類型參數 T1, T2 與泛型類 Point 中的 T1, T2 沒有必然的聯系,也可以使用其他的標識符代替:

public static <V1, V2> void printPoint(V1 x, V2 y){
    V1 m = x;
    V2 n = y;
    System.out.println("This point is:" + m + ", " + n);
}

泛型接口

在Java中也可以定義泛型接口,這里不再贅述,僅僅給出示例代碼:

public class Demo {
    public static void main(String arsg[]) {
        Info<String> obj = new InfoImp<String>("www.weixueyuan.net");
        System.out.println("Length Of String: " + obj.getVar().length());
    }
}

//定義泛型接口
interface Info<T> {
    public T getVar();
}

//實現接口
class InfoImp<T> implements Info<T> {
    private T var;

    // 定義泛型構造方法
    public InfoImp(T var) {
        this.setVar(var);
    }

    public void setVar(T var) {
        this.var = var;
    }

    public T getVar() {
        return this.var;
    }
}

類型擦除

如果在使用泛型時沒有指明數據類型,那么就會擦除泛型類型,請看下面的代碼:

public class Demo {
    public static void main(String[] args){
        Point p = new Point();  // 類型擦除
        p.setX(10);
        p.setY(20.8);
        int x = (Integer)p.getX();  // 向下轉型
        double y = (Double)p.getY();
        System.out.println("This point is:" + x + ", " + y);
    }
}

class Point<T1, T2>{
    T1 x;
    T2 y;
    public T1 getX() {
        return x;
    }
    public void setX(T1 x) {
        this.x = x;
    }
    public T2 getY() {
        return y;
    }
    public void setY(T2 y) {
        this.y = y;
    }
}

運行結果:
This point is:10, 20.8

因為在使用泛型時沒有指明數據類型,為了不出現錯誤,編譯器會將所有數據向上轉型為 Object,所以在取出坐標使用時要向下轉型,這與本文一開始不使用泛型沒什么兩樣。

限制泛型的可用類型

在上面的代碼中,類型參數可以接受任意的數據類型,只要它是被定義過的。但是,很多時候我們只需要一部分數據類型就夠了,用戶傳遞其他數據類型可能會引起錯誤。例如,編寫一個泛型函數用于返回不同類型數組(Integer 數組、Double 數組、Character 數組等)中的最大值:

public <T> T getMax(T array[]){
    T max = null;
    for(T element : array){
        max = element.doubleValue() > max.doubleValue() ? element : max;
    }
    return max;
}

上面的代碼會報錯,doubleValue() 是 Number 類的方法,不是所有的類都有該方法,所以我們要限制類型參數 T,讓它只能接受 Number 及其子類(Integer、Double、Character 等)。

通過 extends 關鍵字可以限制泛型的類型,改進上面的代碼:

public <T extends Number> T getMax(T array[]){
    T max = null;
    for(T element : array){
        max = element.doubleValue() > max.doubleValue() ? element : max;
    }
    return max;
}

<T extends Number> 表示 T 只接受 Number 及其子類,傳入其他類型的數據會報錯。這里的限定使用關鍵字 extends,后面可以是類也可以是接口。但這里的 extends 已經不是繼承的含義了,應該理解為 T 是繼承自 Number 類的類型,或者 T 是實現了 XX 接口的類型。

類型參數的范圍

在泛型中,如果不對類型參數加以限制,它就可以接受任意的數據類型,只要它是被定義過的。但是,很多時候我們只需要一部分數據類型就夠了,用戶傳遞其他數據類型可能會引起錯誤。例如,編寫一個泛型函數用于返回不同類型數組(Integer 數組、Double 數組等)中的最大值:

public <T> T getMax(T array[]){
    T max = null;
    for(T element : array){
        max = element.doubleValue() > max.doubleValue() ? element : max;
    }
    return max;
}

上面的代碼會報錯,doubleValue() 是 Number 類及其子類的方法,不是所有的類都有該方法,所以我們要限制類型參數 T,讓它只能接受 Number 及其子類(Integer、Double、Character 等)。

通過 extends 關鍵字可以限制泛型的類型的上限,改進上面的代碼:

public <T extends Number> T getMax(T array[]){
    T max = null;
    for(T element : array){
        max = element.doubleValue() > max.doubleValue() ? element : max;
    }
    return max;
}

<T extends Number> 表示 T 只接受 Number 及其子類,傳入其他類型的數據會報錯。這里的限定使用關鍵字 extends,后面可以是類也可以是接口。如果是類,只能有一個;但是接口可以有多個,并以“&”分隔,例如 <T extends Interface1 & Interface2>。

這里的 extends 關鍵字已不再是繼承的含義了,應該理解為 T 是繼承自 Number 類的類型,或者 T 是實現了 XX 接口的類型。

通配符(?)

要定義一個泛型類來表示坐標,坐標可以是整數、小數或字符串,請看下面的代碼:

class Point<T1, T2>{
    T1 x;
    T2 y;
    public T1 getX() {
        return x;
    }
    public void setX(T1 x) {
        this.x = x;
    }
    public T2 getY() {
        return y;
    }
    public void setY(T2 y) {
        this.y = y;
    }
}

現在要求在類的外部定義一個 printPoint() 方法用于輸出坐標,怎么辦呢?

可以這樣來定義方法:

public void printPoint(Point p){
    System.out.println("This point is: " + p.getX() + ", " + p.getY());
}

我們知道,如果在使用泛型時沒有指名具體的數據類型,就會擦除泛型類型,并向上轉型為 Object,這與不使用泛型沒什么兩樣。上面的代碼沒有指明數據類型,相當于:

public void printPoint(Point<Object, Object> p){
    System.out.println("This point is: " + p.getX() + ", " + p.getY());
}

為了避免類型擦除,可以使用通配符(?):

public void printPoint(Point<?, ?> p){
    System.out.println("This point is: " + p.getX() + ", " + p.getY());
}

通配符(?)可以表示任意的數據類型。將代碼補充完整:

public class Demo {
    public static void main(String[] args){
        Point<Integer, Integer> p1 = new Point<Integer, Integer>();
        p1.setX(10);
        p1.setY(20);
        printPoint(p1);
      
        Point<String, String> p2 = new Point<String, String>();
        p2.setX("東京180度");
        p2.setY("北緯210度");
        printPoint(p2);
    }
   
    public static void printPoint(Point<?, ?> p){  // 使用通配符
        System.out.println("This point is: " + p.getX() + ", " + p.getY());
    }
}

class Point<T1, T2>{
    T1 x;
    T2 y;
    public T1 getX() {
        return x;
    }
    public void setX(T1 x) {
        this.x = x;
    }
    public T2 getY() {
        return y;
    }
    public void setY(T2 y) {
        this.y = y;
    }
}

運行結果:
This point is: 10, 20
This point is: 東京180度, 北緯210度

但是,數字坐標與字符串坐標又有區別:數字可以表示x軸或y軸的坐標,字符串可以表示地球經緯度。現在又要求定義兩個方法分別處理不同的坐標,一個方法只能接受數字類型的坐標,另一個方法只能接受字符串類型的坐標,怎么辦呢?

這個問題的關鍵是要限制類型參數的范圍,請先看下面的代碼:

public class Demo {
    public static void main(String[] args){
        Point<Integer, Integer> p1 = new Point<Integer, Integer>();
        p1.setX(10);
        p1.setY(20);
        printNumPoint(p1);
      
        Point<String, String> p2 = new Point<String, String>();
        p2.setX("東京180度");
        p2.setY("北緯210度");
        printStrPoint(p2);
    }
   
    // 借助通配符限制泛型的范圍
    public static void printNumPoint(Point<? extends Number, ? extends Number> p){
        System.out.println("x: " + p.getX() + ", y: " + p.getY());
    }
   
    public static void printStrPoint(Point<? extends String, ? extends String> p){
        System.out.println("GPS: " + p.getX() + "," + p.getY());
    }
}

class Point<T1, T2>{
    T1 x;
    T2 y;
    public T1 getX() {
        return x;
    }
    public void setX(T1 x) {
        this.x = x;
    }
    public T2 getY() {
        return y;
    }
    public void setY(T2 y) {
        this.y = y;
    }
}

運行結果:
x: 10, y: 20
GPS: 東京180度,北緯210度

? extends Number 表示泛型的類型參數只能是 Number 及其子類,? extends String 也一樣,這與定義泛型類或泛型方法時限制類型參數的范圍類似。

不過,使用通配符(?)不但可以限制類型的上限,還可以限制下限。限制下限使用 super 關鍵字,例如 <? super Number> 表示只能接受 Number 及其父類

淺談Java泛型之<? extends T>和<? super T>的區別

關于Java泛型,這里我不想總結它是什么,這個百度一下一大堆解釋,各種java的書籍中也有明確的定義,只要稍微看一下就能很快清楚.從泛型的英文名字Generic type也能看出,Generic普通、一般、通用的,是一個概括性的詞,那么泛型從名字上也就好理解了,它是一種通用類型,是java中各種類型的概括.
?是java泛型中的通配符,它代表java中的某一個類,那么<? extends T>就代表類型T的某個子類,<? super T>就代表類型T的某個父類.
這里我們先定義一組有繼承關系的類:
//子類-->父類小紅蘋果--紅蘋果--蘋果--水果--好吃的--吃的

這些類都是左側的類為與它相連接的右側的類的子類.
那么<? extends 蘋果> 代表的是左側藍字和綠字的類中的某個類,而<? super 蘋果>代表的就是綠字和紅字的類中的某個類.
這里要注意的是<? extends T>或是<? super T>代表的是范圍內的某個特定的類,而不是范圍內的所有類.
//所以只要在范圍內,我們可以如下這樣的隨意賦值
List<? extends 蘋果> list1 = new ArrayList<蘋果>();
List<? extends 蘋果> list2= new ArrayList<紅蘋果>();
List<? extends 蘋果> list3 = new ArrayList<小紅蘋果>();

但是對于List<? extends 蘋果> list來說,代表的是一個范圍內的某個類,但是卻不確定是哪個類,所以如果我們向這個list中添加元素的時候:
List<? extends 蘋果> list = new ArrayList<蘋果>();
list.add(蘋果); //編譯錯誤
list.add(紅蘋果); //編譯錯誤
list.add(小紅蘋果); //編譯錯誤

因為編譯器并不知道list到底是哪個類(只有在運行的時候才能確定指代的哪個類),如果list是紅蘋果,那么list.add(蘋果)就將一個父類賦值給子類了,是錯誤的.顯然如果向這個list中添加類,都不能保證是正確的.可能會說小紅蘋果沒有子類,添加小紅蘋果不會錯,但是這只是我定義的一個繼承圖中是這樣,我們完全可以繼續定義個小小紅蘋果來繼承小紅蘋果,這個繼承是沒有下限的.這個反推出一個結論<? extends T>是一個有上限T的類型.那么我們馬上就發現<? super T>實際上是有下限T的類型.
因為對于<? extends T>有上限T,故我們如果list.get(0)一定返回的是T或是T的子類,這個是確定的,得出:
List<? extends 蘋果> list1 = new ArrayList<蘋果>();
蘋果 a = list1.get(0); //這個是一定成立的,編譯也不會有問題
List<? extends 蘋果> list2 = new ArrayList<紅蘋果>();
蘋果 a = list2.get(0);
List<? extends 蘋果> list3 = new ArrayList<小紅蘋果>();
蘋果 a = list3.get(0);

然后我們來看<? super T>,因為它有下限,故我們可以馬上得出,如果向其中添加T類型的對象是沒問題的.因為<? super T>是T的某個父類,將子類T賦值給父類沒任何問題:
List<? super 蘋果> list = new ArrayList<蘋果>();
list.add(蘋果); //無任何問題
List<? super 蘋果> list = new ArrayList<水果>();
list.add(蘋果); //無任何問題

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 228,546評論 6 533
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,570評論 3 418
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 176,505評論 0 376
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,017評論 1 313
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,786評論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,219評論 1 324
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,287評論 3 441
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,438評論 0 288
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,971評論 1 335
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,796評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,995評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,540評論 5 359
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,230評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,662評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,918評論 1 286
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,697評論 3 392
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,991評論 2 374

推薦閱讀更多精彩內容

  • 開發人員在使用泛型的時候,很容易根據自己的直覺而犯一些錯誤。比如一個方法如果接收List作為形式參數,那么如果嘗試...
    時待吾閱讀 1,068評論 0 3
  • 為什么需要泛型? 通過泛型可以定義類型安全的數據結構,而無須使用實際的數據類型(可擴展)。這能夠顯著提高性能并得到...
    一只好奇的茂閱讀 1,271評論 2 39
  • 一、泛型簡介1.引入泛型的目的 了解引入泛型的動機,就先從語法糖開始了解。 語法糖 語法糖(Syntactic S...
    Android進階與總結閱讀 1,030評論 0 9
  • 泛型的好處 使用泛型的好處我覺得有兩點:1:類型安全 2:減少類型強轉 下面通過一個例子說明: 假設有一個Tes...
    德彪閱讀 1,134評論 0 0
  • 初伏 第5天總結 驚嘆 我不知道如何形容我的驚嘆程度,當我看到大家只用弗洛茲一個圖樣創作的作品時,完全超出想象!所...
    木之慧閱讀 4,333評論 5 39