Java基礎(chǔ)-泛型的使用及泛型實(shí)現(xiàn)原理

在泛型出現(xiàn)以前,類和方法只能接受具體的類型。假設(shè)我們自己實(shí)現(xiàn)一個(gè)簡(jiǎn)單的ArrayList,用來持有類A的實(shí)例,它可能是這樣子的:

class A {}

class ArrayListA {
    private int size = 0;

    private A[] array = new A[100];
    
    public void add(A a) {
        array[size++] = a;
    }
    
    public A get(int index) {
        return array[index];
    }
}

現(xiàn)在如果需要一個(gè)ArrayList來持有類B的實(shí)例,由于沒有泛型,那只能把同樣的代碼再寫一遍,并將其中的A全部換成B。難道我們要為每一個(gè)類都寫一個(gè)ArrayList嗎,顯然是不可能的。在泛型出現(xiàn)以前,jdk的ArrayList使用的方法是用Object作為類型參數(shù),這樣使用者需要自己做轉(zhuǎn)型,就像下面這樣:

public class GenericLearn {

    public static void main(String[] args) {
        ArrayList arrayList = new ArrayList();
        arrayList.add("aaa");
        String str = (String) arrayList.get(0);
    }

}

這樣的向下轉(zhuǎn)型,既不方便,也不安全。有沒有一種辦法,能讓類型作為一種可選參數(shù),使得一套代碼能復(fù)用于多個(gè)類,且不需要自己做轉(zhuǎn)型等動(dòng)作,這便是泛型要解決的問題。

泛型的使用

1. 泛型類

class Holder<T> {
    
    private T t;

    public T getT() {
        return t;
    }

    public void setT(T t) {
        this.t = t;
    }
    
    public void print() {
        System.out.println(t);
    }
}

上面是一個(gè)簡(jiǎn)單的泛型類,用一個(gè)<>來指明參數(shù)化類型。現(xiàn)在我們?cè)谑褂肏older類時(shí)就可以指明類型,一旦類型被確定,它就不能用于其他類:

Holder<String> holder = new Holder<>();
// 編譯錯(cuò)誤
holder.setT(1);

2. 泛型接口

interface Handler<T> {

   void handle(T t);
}

class StringHandler implements Handler<String> {

   @Override
   public void handle(String s) {
       // doNothing
   }
}

泛型用于接口和用于類的方式差不多。

3. 泛型方法

泛型也可以直接用于方法:

class BatchUtil {
    
    public static <T> void batchExec(List<T> list, int batchSize, Consumer<List<T>> action) {
        for (int i = 0; i < list.size(); i += batchSize) {
            int endIndex = i + batchSize > list.size() ? list.size() : i + batchSize;
            List<T> tempList = list.subList(i, endIndex);
            action.accept(tempList);
        }
    }
}

以上方法定義了一個(gè)批量操作的工具類,你可以像這樣使用它:

public class GenericLearn {

    public static void main(String[] args) {
        List<Integer> list = Lists.newArrayList(1, 2, 3, 4, 5, 6, 7);
        BatchUtil.batchExec(list, 3, System.out::println);
    }
}

輸出:

[1, 2, 3]
[4, 5, 6]
[7]

4. 泛型邊界

利用extends關(guān)鍵字,可以為泛型參數(shù)限定上邊界。

public class GenericLearn {

    public static void main(String[] args) {
        Holder<A> holder1 = new Holder<>();
        Holder<B> holder2 = new Holder<>();
        // 編譯錯(cuò)誤
        Holder<String> holder3 = new Holder<>();
    }
}

class Holder<T extends A> {
    
    private T t;

    public T getT() {
        return t;
    }

    public void setT(T t) {
        this.t = t;
    }
}

class A {}

class B extends A{}

限定了邊界后,泛型的類型就只能是指定類型及其子類。

5. 通配符

上面介紹的泛型類、接口、方法等都是如何定義泛型,至于使用泛型,最通常的就是指定泛型參數(shù),如List<String>,指定了泛型參數(shù)為String。但是有時(shí)候我們會(huì)碰到這樣的情況:

public class GenericLearn {

    public static void main(String[] args) {
        List<A> listA = new ArrayList<>();
        List<B> listB = new ArrayList<>();
        print(listA);
        // 編譯錯(cuò)誤
        print(listB);
    }
    
    public static void print(List<A> list) {
        for (A a : list) {
            System.out.println(a);
        }
    }
}

class A {}

class B extends A{}

方法接受參數(shù)List<A>,卻無法將List<B>作為入?yún)ⅲ@是因?yàn)殡m然B可以向上轉(zhuǎn)型為A,List<B>卻無法向上轉(zhuǎn)型為List<A>,為了解決這一問題,引入了通配符。

    public static void print(List<? extends A> list) {
        for (A a : list) {
            System.out.println(a);
        }
    }

將方法改寫為這樣后,可以編譯運(yùn)行。但是由此也會(huì)帶來一個(gè)副作用:

public static void print(List<? extends A> list) {
        for (A a : list) {
            System.out.println(a);
        }
        // 編譯錯(cuò)誤
        list.add(new A());
        list.add(new B());
        list.add(new Object());
}

用了通配符后,無法對(duì)List進(jìn)行add操作,這是因?yàn)長ist的add方法,其參數(shù)是泛型類,而通配符僅僅指定了泛型類的上界,因此任何以泛型類為入?yún)⒌姆椒ǘ紵o法使用。這是可以理解的,因?yàn)榧偃缥覀儌魅氲氖荓ist<B>,那就不能往其中加入A的實(shí)例。

這是用通配符指定上界的情況,通配符也可以指定下界,還是以List為例:

public static void main(String[] args) {
        List<? super B> list = new ArrayList<>();
        list.add(new B());
        Object obj = list.get(0);
        // 編譯錯(cuò)誤
        list.add(new A());
        list.add(new Object());
        B b = list.get(0);
        A a = list.get(0);
}

用通配符指定下界后,可以執(zhí)行add操作,但是只可以add類B的實(shí)例,因?yàn)槲覀儾恢繪ist持有的具體類型是什么,只知道它是B或其超類,在這樣的條件下,只有往其中加入類B的實(shí)例是安全的,并且從其中拿到的對(duì)象只能當(dāng)作Object來使用。

通配符也可以不指定邊界,稱為無界通配符,以List為例,對(duì)List<?>,不能執(zhí)行add操作,從中取出的對(duì)象只能當(dāng)作Object來使用。

對(duì)于通配符的使用有一個(gè)PECS原則(Producer Extends, Consumer Super),即如果將泛型類作為生產(chǎn)者使用,例如使用List的get方法,則用上界通配符;如果將泛型類作為消費(fèi)者使用,例如使用List的add方法,則用下界通配符。

泛型的實(shí)現(xiàn)原理

很多人把Java的泛型稱為偽泛型,因?yàn)镴ava的泛型只是編譯期的泛型,一旦編譯成字節(jié)碼,泛型就被擦除了,即在Java中使用泛型,我們無法在運(yùn)行期知道泛型的類型,因此像下面這樣的操作是不行的。

class Holder<T> {
    // 編譯錯(cuò)誤
    private T t = new T();
    private T[] array = new T[0];
    private Class<T> clazz = T.class;
}

因?yàn)樵趈vm運(yùn)行程序時(shí),類Holder是不帶有泛型類T的具體類型的,所以任何需要具體類型的操作,比如實(shí)例化對(duì)象等,都無法進(jìn)行。

為了直觀的理解泛型擦除,我們可以看一下Holder的字節(jié)碼:

class Holder {


  // access flags 0x2
  // signature TT;
  // declaration: T
  private Ljava/lang/Object; t

  @groovyx.ast.bytecode.Bytecode
  void <init>() {
    aload 0
    INVOKESPECIAL java/lang/Object.<init> ()V
    return
  }

  @groovyx.ast.bytecode.Bytecode
  public Object getT() {
    aload 0
    getfield 'Holder.t','Ljava/lang/Object;'
    areturn
  }

  @groovyx.ast.bytecode.Bytecode
  public void setT(Object a) {
    aload 0
    aload 1
    putfield 'Holder.t','Ljava/lang/Object;'
    return
  }
}

可以看到一旦編譯成字節(jié)碼,泛型將被取代為Object。比較一下直接使用Object類和使用泛型的區(qū)別:

public class GenericLearn {

    public static void main(String[] args) {
        Holder holder = new Holder();
        holder.setT("abc");
        String str = (String) holder.getT();
    }

}

class Holder {

    private Object t;

    public Object getT() {
        return t;
    }

    public void setT(String t) {
        this.t = t;
    }
}
@groovyx.ast.bytecode.Bytecode
  public static void main(String[] a) {
    _new 'Holder'
    dup
    INVOKESPECIAL Holder.<init> ()V
    astore 1
    aload 1
    ldc "abc"
    INVOKEVIRTUAL Holder.setT (Ljava/lang/String;)V
    aload 1
    INVOKEVIRTUAL Holder.getT ()Ljava/lang/Object;
    checkcast 'java/lang/String'
    astore 2
    return
  }

下面將Holder加入泛型:

public class GenericLearn {

    public static void main(String[] args) {
        Holder<String> holder = new Holder<>();
        holder.setT("abc");
        String str =  holder.getT();
    }

}

class Holder<T> {

    private T t;

    public T getT() {
        return t;
    }

    public void setT(T t) {
        this.t = t;
    }
}
@groovyx.ast.bytecode.Bytecode
  public static void main(String[] a) {
    _new 'Holder'
    dup
    INVOKESPECIAL Holder.<init> ()V
    astore 1
    aload 1
    ldc "abc"
    INVOKEVIRTUAL Holder.setT (Ljava/lang/Object;)V
    aload 1
    INVOKEVIRTUAL Holder.getT ()Ljava/lang/Object;
    checkcast 'java/lang/String'
    astore 2
    return
  }

可以看到,兩段代碼的字節(jié)碼基本是完全一樣的,注意字節(jié)碼中的checkcast,在不使用泛型時(shí),我們需要將Object手動(dòng)轉(zhuǎn)型成String,而在使用泛型后,我們不需要自己轉(zhuǎn)型,但實(shí)際上我們get到的對(duì)象仍然是Object類型的,只不過編譯器會(huì)自動(dòng)幫我們加入這個(gè)轉(zhuǎn)型動(dòng)作。

再來看一下使用泛型上界的情況:

class Holder<T extends A> {

    private T t;

    public T getT() {
        return t;
    }

    public void setT(T t) {
        this.t = t;
    }
}

class A {}
class Holder {


  // access flags 0x2
  // signature TT;
  // declaration: T
  private LA; t

  @groovyx.ast.bytecode.Bytecode
  void <init>() {
    aload 0
    INVOKESPECIAL java/lang/Object.<init> ()V
    return
  }

  @groovyx.ast.bytecode.Bytecode
  public A getT() {
    aload 0
    getfield 'Holder.t','LA;'
    areturn
  }

  @groovyx.ast.bytecode.Bytecode
  public void setT(A a) {
    aload 0
    aload 1
    putfield 'Holder.t','LA;'
    return
  }
}

在有了泛型上界后,泛型將被擦除成定義的上界。

現(xiàn)在可以對(duì)Java的泛型做一個(gè)總結(jié):Java的泛型只存在于編譯期,一旦編譯成字節(jié)碼,泛型將被擦除。泛型的作用在于在編譯階段保證我們使用了正確的類型,并且由編譯器幫我們加入轉(zhuǎn)型動(dòng)作,使得轉(zhuǎn)型是不需要關(guān)心且安全的。

Java之所以用擦除來實(shí)現(xiàn)泛型,是因?yàn)镴ava是在1.5引入的泛型,為了兼容性,即以前沒有泛型的程序能運(yùn)行在新一代的jvm上,且讓開發(fā)者可以以自己的進(jìn)度將代碼加入泛型特性,而選擇了擦除這一辦法。具體可以參考《Java編程思想》的泛型章節(jié)或知乎上的這篇回答:https://www.zhihu.com/question/28665443/answer/118148143

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

推薦閱讀更多精彩內(nèi)容