Java泛型語法,原理,高級用法和局限

前言

泛型是現(xiàn)代編程語言很重要的組成部分,它的出現(xiàn)有兩個目的:

  • 提供更嚴密的編譯器類型檢查
  • 支持泛型編程

Java從JDK5開始添加了對泛型支持,本文將詳述Java(JDK8)中泛型的語法,原理,高級用法和局限.

正文

泛型類和方法的定義

    //非嚴謹定義,僅表示基本結構
    /**
    *泛型類
    */
    public class class_name<T1, T2, ..., Tn> { /* ... */ }
    
    /**
    *泛型方法
    */
    public <T1,T2,...,Tn> void method_name(T1 t1,T2 t2,...Tn){
     /* ... */ }
    
    //eg.
    
    //創(chuàng)建單Parameter的泛型類
    class A<T>{ /* ... */ }
    //創(chuàng)建多Parameter泛型類
    class B<T,V>{ /* ... */ }


?    
    class A<T> {
            //使用類Type Parameter的方法
            public void method1(T t) {
                /* ... */
            }
        //泛型方法
            public <R> void method2(R r){
                /* ... */
            }
            //混合類Type Parameter和方法Type Parameter的方法
            public <R> void method3(R r, T t) {
                /* ... */
            }
        }

?

T1,T2,...Tn被稱為 Type Parameter(類型參數(shù),詳見Type Parameter 和 Type Argument )

泛型類和方法的使用

Parameterized Types和Raw Types

如果生成泛型類對象時指定了具體的Type Argument,就稱之為Parameterized Types,例如下面這句就生成了Parameterized Types為Integer的ArrayList.

new ArrayList<Integer>();

如果生成時沒有指定Type Argument,就稱為 Raw Types,例如下標這句生成了Raw Types的ArrayLis

//編譯時會提示  Unchecked assignment: 'java.util.ArrayList' to 'java.util.List<java.lang.Integer>'
List<Integer> list=new ArrayList();

為什么會有Raw Types呢? 假設現(xiàn)在有一個JDK5之前編寫的方法,在Raw Types的支持下,老代碼無需修改就能獲得泛型的便利,我們可以直接這樣寫,一方面減輕了用戶更新JDK版本的難度,另一方面保證了JDK的兼容性.

public List getList(){/*...*/}

List<String> result=getList()

基本用法

    /**
    *創(chuàng)建泛型類對象
    */
    new class_name()<>();
    
    /**
    *手動指定Type Argument的泛型方法
    */
    obj.<Type argument>method_name(args)


?    
    class A<T> {
            //使用類Type Parameter的方法
            public void method1(T t) {}
        //泛型方法
            public <R> void method2(R r){}
            //混合類Type Parameter和方法Type Parameter的方法
            public <R> void method3(R r, T t) {}
        }
    
    //創(chuàng)建泛型類對象
    A<String> a = new A<>();
    //1 Parameterized Types在泛型類對象創(chuàng)建的時候已經(jīng)確定了,A的  Parameterized Types是String
    a.method1("abc");
    //2 Parameterized Types會根據(jù)傳入argument的類型自動推導為String
    a.method2("abc");
    //3 顯式指定了Parameterized Types,不再使用自動推導.簽名為method1(Long t)
    a.<Long> method2(0L);
    //4 來自泛型類的Type Parameter由泛型類確定,方法自身的泛型參數(shù)由傳入的argument決定
    a.method3(0, "abc");

Java中的泛型實現(xiàn)原理

Java中的泛型使用類型擦除方式實現(xiàn),JVM并不知道泛型,所有的泛型在編譯階段就已經(jīng)被處理成了普通類和方法,并體現(xiàn)在生成字節(jié)碼上

擦除原理代碼演示

public static void eraseTest() {
        List l1 = new ArrayList();
        l1.add("abc");
        l1.add(Integer.valueOf(123));
        String s1 = (String) l1.get(0);
        Integer i1 = (Integer) l1.get(1);
        List<String> l2 = new ArrayList();
        l2.add("abc");
        // IDE報錯提示: 類型不符
        // l2.add(Integer.valueOf(123));
        String s2 = l2.get(0);
        List<String> l3 = new ArrayList<>();
        l3.add("abc");
        // IDE報錯提示: 類型不符
        // l3.add(Integer.valueOf(123));
        String s3 = (String) l1.get(0);
    }

//對應的字節(jié)碼
public static void eraseTest();
    descriptor: ()V
    flags: ACC_PUBLIC, ACC_STATIC
    Code:
      stack=2, locals=7, args_size=0
         0: new           #2                  // class java/util/ArrayList
         3: dup
         4: invokespecial #3                  // Method java/util/ArrayList."<init>":()V
         7: astore_0
         8: aload_0
         9: ldc           #4                  // String abc
        11: invokeinterface #5,  2            // InterfaceMethod java/util/List.add:(Ljava/lang/Object;)Z
        16: pop
        17: aload_0
        18: bipush        123
        20: invokestatic  #6                  // Method java/lang/Integer.valueOf:(I)Ljava/lang/Integer;
        23: invokeinterface #5,  2            // InterfaceMethod java/util/List.add:(Ljava/lang/Object;)Z
        28: pop
        29: aload_0
        30: iconst_0
        31: invokeinterface #7,  2            // InterfaceMethod java/util/List.get:(I)Ljava/lang/Object;
        36: checkcast     #8                  // class java/lang/String
        39: astore_1
        40: aload_0
        41: iconst_1
        42: invokeinterface #7,  2            // InterfaceMethod java/util/List.get:(I)Ljava/lang/Object;
        47: checkcast     #9                  // class java/lang/Integer
        50: astore_2
        51: new           #2                  // class java/util/ArrayList
        54: dup
        55: invokespecial #3                  // Method java/util/ArrayList."<init>":()V
        58: astore_3
        59: aload_3
        60: ldc           #4                  // String abc
        62: invokeinterface #5,  2            // InterfaceMethod java/util/List.add:(Ljava/lang/Object;)Z
        67: pop
        68: aload_3
        69: iconst_0
        70: invokeinterface #7,  2            // InterfaceMethod java/util/List.get:(I)Ljava/lang/Object;
        75: checkcast     #8                  // class java/lang/String
        78: astore        4
        80: new           #2                  // class java/util/ArrayList
        83: dup
        84: invokespecial #3                  // Method java/util/ArrayList."<init>":()V
        87: astore        5
        89: aload         5
        91: ldc           #4                  // String abc
        93: invokeinterface #5,  2            // InterfaceMethod java/util/List.add:(Ljava/lang/Object;)Z
        98: pop
        99: aload_0
       100: iconst_0
       101: invokeinterface #7,  2            // InterfaceMethod java/util/List.get:(I)Ljava/lang/Object;
       106: checkcast     #8                  // class java/lang/String
       109: astore        6
       111: return

使用綁定的類型(或Object)替換泛型中的Type Parameter,這樣生成的字節(jié)碼中,只包含原始的類,接口和方法

首先看 0~4,51~55,80~84 三種創(chuàng)建泛型類對象的方式生成的字節(jié)碼是相同的,都是

// Method java/util/ArrayList."<init>":()V

再看11,62,93 ,add方法的簽名也都相同,參數(shù)都是Object而非具體的Type Argument

InterfaceMethod java/util/List.add:(Ljava/lang/Object;)Z

為了保證類型安全,在必要的地方加入類型轉換

36,75,106中 36對應的字節(jié)碼checkcast 是由源代碼編譯出的,而75,106都是編譯器對泛型處理后自動生成的

在有繼承的泛型類中使用橋接方法(bridge methods)保證多態(tài)

class A {
        public <T> void method(T t) {

        }

    }

    class B extends A {
        @Override
        public <T> void method(T t) {

        }
    }
//類A對應字節(jié)碼
public void method(T);
    descriptor: (Ljava/lang/Object;)V
    flags: ACC_PUBLIC
    Code:
      stack=0, locals=2, args_size=2
         0: return
      LineNumberTable:
        line 15: 0
    Signature: #14                          // (TT;)V

//類B對應字節(jié)碼
public void method(java.lang.String);
    descriptor: (Ljava/lang/String;)V
    flags: ACC_PUBLIC
    Code:
      stack=0, locals=2, args_size=2
         0: return
      LineNumberTable:
        line 23: 0
  //橋接方法
  public void method(java.lang.Object);
    descriptor: (Ljava/lang/Object;)V
    flags: ACC_PUBLIC, ACC_BRIDGE, ACC_SYNTHETIC
    Code:
      stack=2, locals=2, args_size=2
         0: aload_0
         1: aload_1
         2: checkcast     #3                  // class java/lang/String
         5: invokevirtual #4                  // Method method:(Ljava/lang/String;)V
         8: return
      LineNumberTable:
        line 19: 0

泛型擦除后,A中method簽名為 method(Object) 而B繼承Parameterized Types為String的A后,重寫的method簽名為method(String),這就導致多態(tài)出現(xiàn)問題, 為了解決這個問題,編譯器會自動生成一個橋接方法, 簽名為 method(Object),這個方法內(nèi)部去調(diào)用B重寫的method. 保證多態(tài).

高級用法

定義時限定Type Parameter上界

限定上界,extends后如果跟隨多個,第一個必須為類或抽象類,后面的必須為接口,使用時Type Argument 必須同時滿足所有限定條件

/**
*限定上界的類定義
*/
class class_name <T extends class_or_ abstract_class_name&interface_name&interface_name>
/**
* 限定上界的方法定義
*/
public <T extends class_or_ abstract_class_name&interface_name&interface_name> void method2(T t)

//eg.
class C<T extends Number> extends ArrayList<T> {

}

public <T extends Number & Serializable> void method2(T t) {

}

沒有限定下屆的語法,個人認為是意義不大,所以沒有.

通配符?在泛型中的應用

PECS(Producer Extends Consumer Super)規(guī)則

  1. 如果一個對象頻繁向外讀取數(shù)據(jù)的,就被稱為 "in" ,適合用上界 extends
  2. 如果一個對象經(jīng)常向里插入數(shù)據(jù),就被稱為"out"適合用下界super

場景設定: Plate可以裝載物品, Food,Fruit,Apple,Banana有繼承關系.

static class Plate<T> {
        private T t;

        public T getT() {
            return t;
        }

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

    static class Food {

    }

    static class Fruit extends Food {

    }

    static class Apple extends Fruit {

    }

    static class Banana extends Fruit {

    }

限定上界

Image-extend.png

Plate<Apple> applePlate = new Plate<>();
applePlate.setT(new Apple());
Plate<? extends Fruit> p = applePlate;
// 編譯錯誤,類型不符
// p.setT(new Apple());
Fruit f = p.getT();
f.printName();

不能存,只能取, 且取出的是上界對應的對象. <? extends Fruit>只知道里面存的是Fruit或Fruit的子類,標記為#A,具體是什么不知道,想插入數(shù)據(jù)時不知道和#A是否匹配,所以存不進去,取出時同理,只能當做Fruit取出.

限定下界

Image-super.png

Plate<? super Fruit> plate = new Plate<>();
plate.setT(new Fruit());
Object obj = plate.getT();
((Fruit) obj).printName();

能存,存的必須是Fruit或Fruit的直接/間接父類, 可以取,因為Object是所有類的基類,因此只能當做Object取

無界

Plate<Apple> applePlate = new Plate<>();
applePlate.setT(new Apple());
Plate<?> plate = applePlate;
Object obj=plate.getT();
((Apple) obj).printName();

不能存,只能取,且取出時只能為Object.它有兩個用途

  • 泛型類僅用到Object中定義的方法, 例如 Object.hashcode().
  • 泛型類不依賴Type Parameter的類型.

<?>和<Object>是有差別的,<?>只能取,不能存, 而<Object>是可以作為Object存的.

為什么用了通配符?某些場景下不能存,某些可以

上面講過類型擦除為了保證類型安全,在必要的地方加入類型轉換,這就要有有一個確定的類型,<? extends X> 可以確保是X或X的子類,由于類的繼承原理: X的子類可以強轉為X,添加強轉是使用X即可.因此可以存,而<? super X>和<?> 都沒有確定的類型,都表示一個范圍,因此不能存.

泛型的繼承關系

泛型不會繼承普通類的繼承關系

Untitled.png

要完成泛型中的繼承,需要使用 ?, ?在繼承中對泛型的作用類似于Object.

Untitled 1.png

擦除的弊端

上面說過,擦除法是在編譯進階擦除類型信息,這也就導致運行時獲取不到類型信息.

  • reifiable type: 運行時全部信息都可以獲取到的types,包含primitive,no-generic,raw(未指定實際類型的泛型),無界類型(如List<?>)
  • no-reifiable type: 信息已在編譯時因類型擦除被移除的類型,例如List<String>和List<Number>,JVM無從得知兩者運行時的差別,很多場景下 non-reifiable是無法使用的

泛型弊端演示

  • 無法編譯的<?>

下面這段代碼是無法編譯通過的,原因上面的完全wildcard講過

void foo(List<?> i) {
        i.set(0, i.get(0));
    }

要解決這個問題,可以添加一個輔助方法

void foo(List<?> i) {
        forHelp(i);
    }

    <T> void forHelp(List<T> i) {
        i.set(0, i.get(0));
    }
  • WildcardErrorBad
    void swapFirst(List<? extends Number> l1, List<? extends Number> l2) {
            Number temp = l1.get(0);
            l1.set(0, l2.get(0)); // expected a CAP#1 extends Number,
            // got a CAP#2 extends Number;
            // same bound, but different types
            l2.set(0, temp);        // expected a CAP#1 extends Number,
            // got a Number
        }

這邊這段代碼從語法上就是錯誤的,但是報錯信息可能會讓人迷惑,現(xiàn)在假設有這些數(shù)據(jù)

    List<Integer> l1 = Arrays.asList(1, 2, 3);
    List<Double>  l2 = Arrays.asList(10.10, 20.20, 30.30);
    swapFirst(l1, l2);

很明顯, l2中的數(shù)據(jù)是Double類型,而l1中的數(shù)據(jù)時Integer類型,當然無法set.

  • Cannot Instantiate Generic Types with Primitive Types原始類型無法作為泛型實例化Type
    //compile error!
    List<int,int> il
  • Cannot Create Instances of Type Parameters無法創(chuàng)建Type Parameters的實例
    public static <E> void append(List<E> list) {
        E elem = new E();  // compile-time error
        list.add(elem);
    }

一種可行的解決方案

    public static <E> void append(List<E> list, Class<E> cls) throws Exception {
        E elem = cls.newInstance();   // OK
        list.add(elem);
    }
  • Cannot Declare Static Fields Whose Types are Type Parameters不能聲明泛型靜態(tài)域
    //compile error
    public class MobileDevice<T> {
        private static T os;

        // ...

    }
  • Cannot Use Casts or instanceof With Parameterized Types不能對Parameterized Type使用cast或instanceof
    //ArrayList<Integer>, ArrayList<String> ,LinkedList<Character>都可能被傳遞進去,而runtime并不會追蹤Type Parameters
    public static <E> void rtti(List<E> list) {
        if (list instanceof ArrayList<Integer>) {  // compile-time error
            // ...
        }
    }
    
    // compile-time error
    List<Integer> li = new ArrayList<>();
    List<Number>  ln = (List<Number>) li;  
    
    
    //OK
    List<String> l1 = ...;
    ArrayList<String> l2 = (ArrayList<String>)l1;
  • Cannot Create Arrays of Parameterized Types不能創(chuàng)建Parameterized Type的數(shù)組,編譯期被禁止,因為運行時無法分辨
    Object[] stringLists = new List<String>[2];  // compiler error, but pretend it's allowed
    stringLists[0] = new ArrayList<String>();   // OK
    stringLists[1] = new ArrayList<Integer>();  // An ArrayStoreException should be thrown,
                                                // but the runtime can't detect it.
  • Cannot Create, Catch, or Throw Objects of Parameterized Types不能創(chuàng)建,捕捉或拋出Parameterized Type
    // Extends Throwable indirectly
    class MathException<T> extends Exception { /* ... */ }    // compile-time error
    
    // Extends Throwable directly
    class QueueFullException<T> extends Throwable { /* ... */ // compile-time error
    
    public static <T extends Exception, J> void execute(List<J> jobs) {
        try {
            for (J job : jobs)
                // ...
        } catch (T e) {   // compile-time error
            // ...
        }
    }
    
    
    class Parser<T extends Exception> {
        public void parse(File file) throws T {     // OK
            // ...
        }
    }
  • Cannot Overload a Method Where the Formal Parameter Types of Each Overload Erase to the Same Raw Type不能重載擦除后raw type相同的方法
    // compile error
    public class Example {
        public void print(Set<String> strSet) { }
        public void print(Set<Integer> intSet) { }
    }

后記

寫這篇文章的起因是在做一個需求的時候,同事用泛型很好的封裝了一個基礎組件,優(yōu)雅簡潔,我卻完全沒有想到,還在傻傻的強轉類型轉換. 很早之前我就研究過泛型并且做了筆記, 趁這次機會回顧實踐一下并寫了這篇文章.


官方文檔

JVM如何理解Java泛型類(轉)

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

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

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