談?wù)凧ava的泛型

前言

JDK1.5號(hào)稱是Java最重要的版本更新,而泛型又是JDK1.5中一個(gè)最重要的特征。
使用泛型機(jī)制編寫的程序代碼要比哪些雜亂地使用Object變量,然后再進(jìn)行強(qiáng)制類型轉(zhuǎn)換的代碼具有更好的安全性和可讀性。泛型對(duì)于集合類尤其有用。
泛型涉及的內(nèi)部機(jī)制比較復(fù)雜,所有分開兩章來講。

泛型入門

1. 泛型解決了什么問題

先看下面的一段代碼

ppublic class GenericDemo {
    public static void main(String[] args) {
        List list = new ArrayList();
        list.add("Winson");
        list.add("Tom");
        list.add(1);
        list.forEach(str -> System.out.println(((String)str).length()));
    }
}

我們創(chuàng)建了一個(gè)list,希望是用來保存String的,但“不小心”把整數(shù)1放入了list中,當(dāng)程序?qū)nteger轉(zhuǎn)成String的時(shí)候會(huì)報(bào)ClassCastException。
上面這種"不小心",編譯器不會(huì)檢查出來。

我們使用泛型改進(jìn)這個(gè)程序

public class GenericDemo {
    public static void main(String[] args) {
        List<String> list = new ArrayList();
        list.add("Winson");
        list.add("Tom");
        list.add(1);    //會(huì)提示該list不接受int類型
        list.forEach(str -> System.out.println(str.length()));//無須強(qiáng)制轉(zhuǎn)換
    }
}
  1. 我們?cè)贚ist后面加了尖括號(hào)并指定了String類型,表示這個(gè)List只能保存String。如果add其他類型,編譯時(shí)就會(huì)檢查出來。
  2. list會(huì)記住所有元素的數(shù)值類型,無須對(duì)集合元素進(jìn)行強(qiáng)制轉(zhuǎn)換。

2. 泛型接口和泛型類類

我們先定義個(gè)簡(jiǎn)單的泛型類和接口

class Apple<T>{
    private T first;
    private T second;
    
    public Apple(T first,T second){
        this.first=first;
        this.second=second;
    }

    public T getFirst() {return first;}
    public T getSecond() {return second;}

    public void setSecond(T second) {this.second =second;}
    public void setFirst(T first) {this.first = first;}
}
public interface List<E>{
    void add(E x);
}
public interface Map<K,V>{
    V put(K key,V value);
}

類型變量

泛型類可以有多個(gè)類型變量。例如:

class Apple<T,U>{...}

類型變量使用大寫單個(gè)大寫的字母:
E表示集合的元素類型
K和V表示關(guān)鍵字和值的類型
T、U和S表示任意類型

3. 泛型方法

class ArrayUtils{
    public static <T> T getMiddle(T... a){
        return a[a.length/2];
    }
}

這個(gè)泛型方法是在一個(gè)普通類中定義的,當(dāng)然,也可以定義在泛型類中。
類型變量放在修飾符(這是public static)的后面,返回類型的前面。

調(diào)用泛型方法

System.out.println(ArrayUtils.<String>getMiddle("a","b","c"));

在這種情況(實(shí)際也是大多數(shù)情況)下,方法調(diào)用中可以省略<String>類型參數(shù),編譯器能自動(dòng)判斷。

System.out.println(ArrayUtils.getMiddle("a","b","c"));

4. 類型變量的限定

我們先看看下面這段代碼

class ArrayUtils{
    public static <T> T min(T[] a){
        if (a==null||a.length==0) return null;
        T smallest =a[0];
        for (int i=1;i<a.length;i++)
            if (smallest.compareTo(a[i])>0) smallest = a[i];
        return smallest;
    }
}

這段代碼存在的問題是如何確定T所屬的類有compareTo方法。
解決這個(gè)問題的方法是將T限定為實(shí)現(xiàn)了Comparable接口的類:

public static <T extends Comparable> T min(T[] a)

為什么使用extends而不是implements呢?
T是綁定類型的子類型(subtype)
T和綁定類型可以是類、也可以是接口
選用關(guān)鍵字extends原因是更接近子類的概念,而且Java的設(shè)計(jì)者也不打算在語言中在添加一個(gè)新的關(guān)鍵字(如sub)

另外,一個(gè)類型變量可以有多個(gè)限定

限定類型用&分隔
類型變量用逗號(hào)分隔

T extends Comparable & Serializable

泛型進(jìn)階

1. 類型擦除

Java的泛型是偽泛型

虛擬機(jī)里沒有泛型類型對(duì)象,所有對(duì)象都是普通類。在編譯期間,所有的泛型信息都會(huì)被擦除(erased)。

什么是擦除?

Java中的泛型基本上都是在編譯器這個(gè)層次來實(shí)現(xiàn)的。在生成的Java字節(jié)碼中是不包含泛型中的類型信息的。使用泛型的時(shí)候加上的類型參數(shù),會(huì)在編譯器在編譯的時(shí)候去掉。這個(gè)過程就稱為類型擦除。

如在代碼中定義的List<object>和List<String>等類型,在編譯后都會(huì)編程List。JVM看到的只是List,而由泛型附加的類型信息對(duì)JVM來說是不可見的。

泛型類在Java源碼上看起來與一般的類不同,在執(zhí)行時(shí)被虛擬機(jī)翻譯成對(duì)應(yīng)的“原始類型”

例子

可以通過兩個(gè)簡(jiǎn)單的例子,來證明java泛型的類型擦除。

public class Test1 {  
    public static void main(String[] args) {  
        ArrayList<String> arrayList1=new ArrayList<String>();  
        arrayList1.add("a");  
        ArrayList<Integer> arrayList2=new ArrayList<Integer>();  
        arrayList2.add(1);  
        System.out.println(arrayList1.getClass()==arrayList2.getClass());  
    }  
}  

在這個(gè)例子中,我們定義了兩個(gè)ArrayList泛型數(shù)組,一個(gè)是ArrayList<String>,一個(gè)是ArrayList<Integer>。
最后,我們通過arrayList1對(duì)象和arrayList2對(duì)象的getClass方法獲取它們的類的信息,最后發(fā)現(xiàn)結(jié)果為true。說明泛型類型String和Integer都被擦除掉了,只剩下了原始類型。

什么是原始類型?

原始類型(raw type)就是擦除去了泛型信息,最后在字節(jié)碼中的類型變量的真正類型。擦除類型變量,并替換為限定定類型(無限定的變量用Object)。
例如,上面的Apple<T>的原始類型如下:

class Apple{
    private Object first;
    private Object second;
    
    public Apple(Object first,Object second){
        this.first=first;
        this.second=second;
    }

    public Object getFirst() {return first;}
    public Object getSecond() {return second;}

    public void setSecond(Object second) {this.second =second;}
    public void setFirst(Object first) {this.first = first;}
}

因T是一個(gè)無限定的變量,所以直接用Object替換。就是一個(gè)普通類,就像泛型引入前實(shí)現(xiàn)的那樣。

翻譯泛型表達(dá)式

我們結(jié)合上面Apple的泛型類,再來看下面的代碼

Apple<Date> apple = new Apple();
Date date = apple.getFirst(); 

擦除后,getFirst返回Object類型,為什么不需要執(zhí)行強(qiáng)制類型轉(zhuǎn)換就可以賦值給Date類型的變量呢?

原因是編譯器會(huì)自動(dòng)插入Date的強(qiáng)制類型轉(zhuǎn)換。 也就是說,編譯器把getFirst方法翻譯成兩條虛擬機(jī)指令:

  • 對(duì)原始方法Apple.getFirst的調(diào)用;
  • 將返回的Object類型強(qiáng)制轉(zhuǎn)換成Employee類型。

當(dāng)存取一個(gè)泛型域時(shí)也會(huì)插入強(qiáng)制類型轉(zhuǎn)換,如:

Date date = apple.first;

翻譯泛型方法

類型的擦除也會(huì)出現(xiàn)在泛型方法中。
我們來看下面的代碼

public class BridgeDemo {
    public static class One<T> {
        public T getT() {
            return null;
        }
    }

    public static class Two extends One<String> {
        public String getT() {
            return null;
        }
    }
}

在泛型擦除后的代碼類似于:

public class BridgeDemo {  
    public static class One {  
        public Object getT() {  
            return null;  
        }  
    }  
  
    public static class Two extends One {  
        public String getT() {  
            return null;  
        }  
    }  
}  

我們來反編譯下Two這個(gè)類

  public BridgeDemo$Two();
    Code:
       0: aload_0
       1: invokespecial #1                  // Method BridgeDemo$One."<init>":()V
       4: return

  public java.lang.String getT();
    Code:
       0: aconst_null
       1: areturn

  public java.lang.Object getT();
    Code:
       0: aload_0
       1: invokevirtual #2                  // Method getT:()Ljava/lang/String;
       4: areturn
}

在反編譯后的輸出中,可以看見有一個(gè)新的合成方法“java.lang.Object getT()”,這是源代碼中沒有的。
這個(gè)方法是一個(gè)橋方法,它負(fù)責(zé)將調(diào)用代理到“java.lang.String getT()”。因?yàn)樵贘VM里,方法的返回類型是方法簽名的一部分,而創(chuàng)建橋方法是實(shí)現(xiàn)協(xié)變返回類型的方式。

我們?cè)诎殉绦蚋囊幌?/p>

public class BridgeDemo {
    public static class One<T> {
        public T getT(T args) {
            return args;
        }
    }
    
    public static class Two extends One<String> {
        public String getT(String args) {
            return args;
        }
    }
}

看下反編譯的結(jié)果

public class BridgeDemo$Two extends BridgeDemo$One<java.lang.String> {
  public BridgeDemo$Two();
    Code:
       0: aload_0
       1: invokespecial #1                  // Method BridgeDemo$One."<init>":()V
       4: return

  public java.lang.String getT(java.lang.String);
    Code:
       0: aload_1
       1: areturn

  public java.lang.Object getT(java.lang.Object);
    Code:
       0: aload_0
       1: aload_1
       2: checkcast     #2                  // class java/lang/String
       5: invokevirtual #3                  // Method getT:(Ljava/lang/String;)Ljava/lang/String;
       8: areturn
}

在這里,橋方法重寫了基類One,它不僅做了有參數(shù)的調(diào)用,同時(shí)還執(zhí)行了到“java.lang.String”的類型轉(zhuǎn)換。這意味著在執(zhí)行下面的代碼忽略編譯器的“uncheck”警告時(shí),橋方法將拋出ClassCastException異常。

public static void main(String[] args) {
    One one = new Two();
    one.getT(new Object());
}

Exception in thread "main" java.lang.ClassCastException: java.lang.Object cannot be cast to java.lang.String

你需要記住的有關(guān)Java泛型轉(zhuǎn)換的事實(shí)

  • 虛擬機(jī)中沒有泛型,只有普通類的方法
  • 所有的類型參數(shù)都用他們的限定類型替換
  • 橋方法被合成來保持多態(tài)
  • 為保持類型安全性,必要時(shí)插入強(qiáng)制類型轉(zhuǎn)換
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容