Java 泛型

一、為什么要用泛型?

1、看一段代碼

List list = new ArrayList();
list.add(1);
list.add("1");
list.add(false);
int m = list.get(0);//無法編譯通過

在不用泛型的情況下,我們可以向list對(duì)象中添加任何對(duì)象,這個(gè)沒問題。但是,當(dāng)我們想獲取數(shù)據(jù)的時(shí)候,麻煩就來了,我們不知道每個(gè)index對(duì)應(yīng)數(shù)據(jù)的具體類型,我們只知道它是個(gè)Object,這往往不能滿足我們的需求。
2、使用泛型的會(huì)怎么樣
還是上面的代碼,稍微做下改動(dòng)

List<String> list = new ArrayList();
list.add(1);//報(bào)錯(cuò)
list.add("1");
list.add(false);//報(bào)錯(cuò)

使用了泛型,編譯器會(huì)幫開發(fā)者做類型檢查,這就保證了此list中添加的數(shù)據(jù)類型全部都是String,那么在取數(shù)據(jù)的時(shí)候,jvm會(huì)幫我們做類型強(qiáng)轉(zhuǎn),因?yàn)椴迦胧墙?jīng)過檢查的,所以獲取的時(shí)候強(qiáng)轉(zhuǎn)是安全的。

二、泛型類、泛型接口、泛型方法

1、先看泛型類

    static class Boy<T>{
        T toy;
    }

T表示某個(gè)類型,但具體是什么類型不知道,所以我們沒辦法將toy初始化,因?yàn)閚ew一個(gè)對(duì)象出來需要知道確切類型,要想解決這個(gè)問題,我們先看下泛型方法
2、泛型方法

  static class Boy<T>{
        T toy;
        public <F extends ToyFactory<T>> T obtainToy(F f){
           this.toy = f.createToy();
           return this.toy;
       }
 }

這個(gè)泛型方法表示函數(shù)obtainToy接受一個(gè)F類型參數(shù),這個(gè)F繼承自泛型接口ToyFactory,返回一個(gè)參數(shù)化的類型T。我們來看下泛型接口

2、泛型接口

  interface ToyFactory<T>{
        T createToy();
  }
  static class Boy<T>{
        T toy;
        public <F extends ToyFactory<T>> T obtainToy(F f){
           this.toy = f.createToy();
           return this.toy;
       }
  }

ToyFactory這個(gè)泛型接口很簡(jiǎn)單,就一個(gè)方法createToy,返回值是一個(gè)參數(shù)化的類型T的對(duì)象。
那假設(shè),現(xiàn)在現(xiàn)在這個(gè)Boy的一個(gè)對(duì)象想要一個(gè)想要一個(gè)玩具船,按照上面的思路,我們應(yīng)該有個(gè)玩具船工廠類繼承自玩具工廠類,專門生產(chǎn)玩具船。
看代碼

 interface ToyFactory<T>{
        T createToy();
  }
  static abstract class Toy{
        public abstract void  play();
  }

 static class ToyBoat extends Toy{
        public void play(){
            Log.d("ToyBoat","ToyBoat surf");
        }
 }
//玩具船工廠函數(shù)
 static class ToyBoatFactory implements ToyFactory<ToyBoat>{
       public ToyBoat createToy(){
           return new ToyBoat();
       }
 }
 static class Boy<T>{
        T toy;
        public <F extends ToyFactory<T>> T obtainToy(F f){
           this.toy = f.createToy();
           return this.toy;
       }
  }
public static void main(String ...args){
        Boy<ToyBoat> boatBoy = new Boy<>();
        boatBoy.obtainToy(new ToyBoatFactory());
}

那現(xiàn)在還有一個(gè)問題,我們給Boy增加一個(gè)函數(shù)

  public void play(){
            toy.play();
  }

這段代碼是無法編譯通過的,因?yàn)閠oy.play()涉及到了具體的類型,而toy是某一個(gè)類型,是不能確定的,那還有辦法能讓這段代碼編譯通過嗎?看下面代碼,我們改造一下Boy類

 static class Boy<T extends Toy>{
        T toy;
        public <F extends ToyFactory<T>> T obtainToy(F f){
           this.toy = f.createToy();
           return this.toy;
       }
  }
  public void play(){
            toy.play();
  }

我們給Boy的泛型結(jié)構(gòu)加了一個(gè)上線,表示這個(gè)參數(shù)化的類型必須繼承自Toy類,因此我們能夠直接調(diào)用父類的函數(shù)。

三、泛型帶來的問題

1、類型擦除
我們新聲明個(gè)泛型類

   static class Girl<T>{
        T toy;
        public void setToy(T t){
            this.toy = t;
        }
    }

編譯成功后,通過javap -c /path/to/.. 名利來看下生成的class文件

class com.debug.ldsplugin.MainActivity$Girl<T> {
  T toy;

  com.debug.ldsplugin.MainActivity$Girl();
    Code:
       0: aload_0
       1: invokespecial #1                  // Method java/lang/Object."<init>":()V
       4: return

  public void setToy(T);
    Code:
       0: aload_0
       1: aload_1
       2: putfield      #2                  // Field toy:Ljava/lang/Object;
       5: return
}

關(guān)鍵是這一行

       2: putfield      #2                  // Field toy:Ljava/lang/Object;

我們看到,toy的類型實(shí)際上是一個(gè)Object類型。那問題來了,上面不是說了toy是個(gè)未知的類型嗎?這涉及到j(luò)ava的泛型機(jī)制,java中的泛型是偽泛型,因?yàn)槲覀兛吹骄幾g后參數(shù)化的類型被擦除了,在這個(gè)例子中擦除到Object。
稍微改下代碼

    static class Girl<T extends Toy>{
        T toy;
        public void setToy(T t){
            this.toy = t;
        }
    }

我們看到泛型被擦除到Toy

       2: putfield      #2                  // Field toy:Lcom/debug/ldsplugin/MainActivity$Toy;

2、類型擦除帶來的問題
上面我們已經(jīng)提到了,涉及到需要明確具體類型的操作無法進(jìn)行。那我們舉一些常見的例子以及給出一些解決方案。

  • 如何創(chuàng)建泛型數(shù)組
    我們嘗試通過常規(guī)方式創(chuàng)建一個(gè)數(shù)組
Girl<ToyBoat>[] girls = new Girl<ToyBoat>[1];//編譯器報(bào)錯(cuò),不讓這么創(chuàng)建

編譯器不讓創(chuàng)建的原因,我猜測(cè)是因?yàn)閖ava中泛型是偽泛型,它不像C#或者其他支持真泛型語言中Girl<ToyBoat>是個(gè)類型,而創(chuàng)建一個(gè)數(shù)組在java中是需要明確類型的,所以,這么做無法編譯通過。那如果就想創(chuàng)建這樣一個(gè)數(shù)組,應(yīng)該怎么做呢?如下:

Girl<ToyBoat>[] girls = (Girl<ToyBoat>[]) new Girl[1];

也就是說我們需要?jiǎng)?chuàng)建一個(gè)類型擦除的數(shù)組,然后再?gòu)?qiáng)轉(zhuǎn)

  • 使用T[] array
    還是先看代碼
    static class GenericArray<T>{
        T[] arr;
        public GenericArray(int size){
            arr = (T[]) new Object[size];
        }

        public T get(int index){
            return arr[index];
        }
        public void set(T t,int index){
            arr[index] = t;
        }

        public T[] rep(){
            return arr;
        }
    }

在構(gòu)造函數(shù)里做了類型強(qiáng)轉(zhuǎn),這里之所以是安全的,是因?yàn)轭愋筒脸?,相?dāng)于(Object[])new Object[size],所以這是安全的,正常get和set也是沒問題,問題出在rep()這個(gè)函數(shù),這個(gè)函數(shù)的調(diào)用有可能引發(fā)崩潰,為什么是說有可能呢?我們還是看代碼

GenericArray<String> genericArray = new GenericArray<>(3);
genericArray.set("aa",0);
genericArray.get(0);
genericArray.rep();

這樣調(diào)用是沒問題的,根據(jù)類型擦除原理我們知道,rep會(huì)返回一個(gè)Object[],我們?cè)谏傻腸lass源碼里也能看到這樣一行代碼

      34: invokevirtual #11                 // Method com/debug/ldsplugin/MainActivity$GenericArray.rep:()[Ljava/lang/Object;

我們把上面代碼稍微改下

 String[] s = genericArray.rep();

這時(shí)候程序就會(huì)崩潰,這是為什么呢?
我們還是看下class源碼

      34: invokevirtual #11                 // Method com/debug/ldsplugin/MainActivity$GenericArray.rep:()[Ljava/lang/Object;
      37: checkcast     #12                 // class "[Ljava/lang/String;"
      40: astore_3

jvm把這段代碼

 String[] s = genericArray.rep();

拆解成了三行代碼,先調(diào)用rep,然后轉(zhuǎn)型,問題就出在轉(zhuǎn)型上,因?yàn)闆]辦法把Object[]轉(zhuǎn)換為String[],所以異常。
為了加深理解,我們?cè)俑南麓a

    static class GenericArray<T extends String>{
        T[] arr;
        public GenericArray(int size){
            arr = (T[]) new Object[size];
        }

        public T get(int index){
            return arr[index];
        }
        public void set(T t,int index){
            arr[index] = t;
        }

        public T[] rep(){
            return arr;
        }
    }

這段代碼在new GenericArray的時(shí)候就會(huì)異常,因?yàn)闃?gòu)造函數(shù)里嘗試把Object[]轉(zhuǎn)為Integer[]這是注定要失敗的。
那么好的選擇應(yīng)該是下面這樣

  static class GenericArray<T extends String>{
        Object[] arr;
        public GenericArray(int size){
            arr = new Object[size];
        }

        public T get(int index){
            return (T)arr[index];
        }
        public void set(T t,int index){
            arr[index] = t;
        }

        public T[] rep(){
            return (T[])arr;
        }
    }

這里面rep方法會(huì)依然報(bào)錯(cuò),但是我們不會(huì)忘記數(shù)組運(yùn)行中的實(shí)際類型是Object,因此會(huì)減少很多麻煩。
那有沒有更好的選擇呢?答案是有,就是使用類型標(biāo)識(shí)

    static class GenericArray<T extends String>{
        T[] arr;
        public GenericArray(Class<T> clz,int size){
            arr = (T[])Array.newInstance(clz,size);
        }

        public T get(int index){
            return (T)arr[index];
        }
        public void set(T t,int index){
            arr[index] = t;
        }

        public T[] rep(){
            return arr;
        }
    }

 GenericArray<String> genericArray = new GenericArray<>(String.class,3);
 genericArray.set("aa",0);
 genericArray.get(0);
 String[] s = genericArray.rep();

這樣所有代碼都能正常工作了

四、泛型通配符

泛型通配符我建議大家來看這篇文章。

?著作權(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)容