深入理解 Java 泛型

[TOC]

深入理解 Java 泛型

概述

泛型的本質(zhì)是參數(shù)化類型,通常用于輸入?yún)?shù)、存儲類型不確定的場景。相比于直接使用 Object 的好處是:編譯期強類型檢查、無需進行顯式類型轉(zhuǎn)換。

類型擦除

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

public class Test {
    public static void main(String[] args) {
        List<String> strList = new ArrayList<>();
        List<Integer> intList = new ArrayList<>();
        System.out.println(strList.getClass().getName());
        System.out.println(intList.getClass().getName());
    }
}

上面這一段代碼,運行后輸出如下,可知在運行時獲取的類型信息是不帶具體類型的:

java.util.ArrayList
java.util.ArrayList

類型擦除也是 Java 的泛型實現(xiàn)方式與 C++ 模板機制實現(xiàn)方式之間的重要區(qū)別。這就導(dǎo)致:

泛型類并沒有自己獨有的Class類對象,只有List.class。
運行時無法獲得泛型的真實類型信息。

比如在 反序列化 Json 串至 List 字符串時,需要這么做:

public class Test {
    public static final ObjectMapper mapper = new ObjectMapper();
    public static void main(String[] args) throws IOException {
        JavaType javaType = getCollectionType(ArrayList.class, String.class);
        List<String> lst = mapper.readValue("[\"hello\",\"world\"]", javaType);
        System.out.println(lst);
    }
    // 獲取泛型的Collection Type
    public static JavaType getCollectionType(Class<?> collectionClass, Class<?>... elementClasses) {
        return mapper.getTypeFactory().constructParametricType(collectionClass, elementClasses);
    }
}

Debug 發(fā)現(xiàn) getCollectionType 方法輸出的是 CollectionType 對象,里面存儲了元素類型 _elementType。這就相當于把 List 的元素類型 String.class 作為參數(shù),提供給了 Jackson 去反序列化。而下面的做法會編譯失?。?/p>

public class Test {
    public static final ObjectMapper mapper = new ObjectMapper();
    public static void main(String[] args) throws IOException {
        List<String> lst = mapper.readValue("[\"hello\",\"world\"]", List<String>.class); // 編譯錯誤
        System.out.println(lst);
    }
}

泛型不是協(xié)變的

在 Java 語言中,數(shù)組是協(xié)變的,也就是說,如果 Integer 擴展了 Number,那么不僅 Integer 是 Number,而且 Integer[] 也是 Number[],在要求 Number[] 的地方完全可以傳遞或者賦予 Integer[]。(更正式地說,如果 Number是 Integer 的超類型,那么 Number[] 也是 Integer[]的超類型)。您也許認為這一原理同樣適用于泛型類型 —— List< Number> 是 List< Integer> 的超類型,那么可以在需要 List< Number> 的地方傳遞 List< Integer>。不幸的是,情況并非如此。為啥呢?這么做將破壞要提供的類型安全泛型。

對于數(shù)組來說,下面的代碼會有運行時錯誤:

public class Test {
    public static void main(String[] args) {
        String[] strArray = new String[3];
        Object[] objArray = strArray;

        objArray[0] = 1;// 運行時錯誤:Exception in thread "main" java.lang.ArrayStoreException: java.lang.Integer  
    }
}

而集合這么寫就會有編譯錯誤:

public class Test {
    public static void main(String[] args) {
        List<String> strList = new ArrayList<>();
        // 編譯 Error:(14, 32) java: 不兼容的類型: java.util.List<java.lang.String>無法轉(zhuǎn)換為java.util.List<java.lang.Object>
        List<Object> objList = strList; 
    }
}

數(shù)組能夠協(xié)變而泛型不能協(xié)變的另一個后果是,不能實例化泛型類型的數(shù)組(new List< String>[3] 是不合法的),除非類型參數(shù)是一個未綁定的通配符(new List< ?>[3]是合法的)。具體可以運行下面的代碼看看:

public class Test {
    public static void main(String[] args) {
        // 編譯正常
        List<?>[] lsa2 = new List<?>[10];
        // 編譯 Error:(14, 30) java: 創(chuàng)建泛型數(shù)組
        List<String>[] lsa = new List<String>[10];
    }
}

構(gòu)造延遲

因為運行時不能區(qū)分 List< String> 和 List< Integer>(運行時都是 List),用泛型類型參數(shù)標識類型的變量的構(gòu)造就成了問題。運行時缺乏類型信息,這給泛型容器類和希望創(chuàng)建保護性副本的泛型類提出了難題。比如:

不能使用類型參數(shù)訪問構(gòu)造函數(shù)

您不能使用類型參數(shù)訪問構(gòu)造函數(shù),因為在編譯的時候還不知道要構(gòu)造什么類,因此也就不知道使用什么構(gòu)造函數(shù)。使用泛型不能表達“T必須擁有一個拷貝構(gòu)造函數(shù)(copy constructor)”(甚至一個無參數(shù)的構(gòu)造函數(shù))這類約束,因此不能使用泛型類型參數(shù)所表示的類的構(gòu)造函數(shù)。

public class Test {
    public <T> void doSomething(T param) {
        T copy = new T(param);  // 編譯錯誤:Error:(13, 22) java: 意外的類型,需要: 類,找到: 類型參數(shù)T
    }
}

不能使用 clone 方法

為什么呢?因為 clone() 在 Object 中是 protected 保護訪問的,調(diào)用 clone() 必須通過將 clone() 改寫為 public 公共訪問的類方法來完成。但是 T 的 clone() 是否為 public 是無法確定的,因此調(diào)用其 clone 也是非法的。

public class Test {
    public <T> void doSomething(T param) {
        T copy = (T) param.clone();  // 編譯 Error:(13, 27) java: clone()在java.lang.Object中訪問protected
    }
}

不能創(chuàng)建泛型數(shù)組

不能實例化用類型參數(shù)表示的類型數(shù)組。編譯器不知道 T 到底表示什么類型,因此不能實例化 T 數(shù)組。

public class Test {
    public <T> void doS() {
        T[] t = new T[5];
    }
}

那么 ArrayList 是如何存儲數(shù)據(jù)的呢?請看下面的源代碼,是用 Object 數(shù)組存儲的,所以在獲取元素時要做顯示類型轉(zhuǎn)換(在 elementData 方法中):

public class ArrayList<E> extends AbstractList<E>
        implements List<E>, RandomAccess, Cloneable, java.io.Serializable
{
    transient Object[] elementData; // Object 數(shù)組存儲數(shù)據(jù)
    public ArrayList(int initialCapacity) {
        if (initialCapacity > 0) {
            this.elementData = new Object[initialCapacity];
        } else if (initialCapacity == 0) {
            this.elementData = EMPTY_ELEMENTDATA;
        } else {
            throw new IllegalArgumentException("Illegal Capacity: "+ initialCapacity);
        }
    }    

    public E get(int index) {
        rangeCheck(index);
        return elementData(index);
    }    
    @SuppressWarnings("unchecked")
    E elementData(int index) {
        return (E) elementData[index];
    }    
}

通配符 extends 和 super

在泛型不是協(xié)變中提到,在使用 List< Number> 的地方不能傳遞 List< Integer>,那么有沒有辦法能讓他兩兼容使用呢?答案是:有,可以使用通配符。

泛型中 ? 可以用來做通配符,單純 ? 匹配任意類型。< ? extends T > 表示類型的上界是 T,參數(shù)化類型可能是 T 或 T 的子類:

public class Test {
    static class Food {}
    static class Fruit extends Food {}
    static class Apple extends Fruit {}

    public static void main(String[] args) throws IOException {
        List<? extends Fruit> fruits = new ArrayList<>();
        fruits.add(new Food());     // compile error
        fruits.add(new Fruit());    // compile error
        fruits.add(new Apple());    // compile error

        fruits = new ArrayList<Fruit>(); // compile success
        fruits = new ArrayList<Apple>(); // compile success
        fruits = new ArrayList<Food>(); // compile error
        fruits = new ArrayList<? extends Fruit>(); // compile error: 通配符類型無法實例化  

        Fruit object = fruits.get(0);    // compile success
    }
}

從上面代碼中可以看出來,賦值是參數(shù)化類型為 Fruit 和其子類的集合都可以成功,通配符類型無法實例化。為啥上面代碼中的 add 全部編譯失敗了呢?因為 fruits 集合并不知道實際類型是 Fruit、Apple 還是 Food,所以無法對其賦值。

除了 extends 還有一個通配符 super,< ? super T > 表示類型的下界是 T,參數(shù)化類型可以是 T 或 T 的超類:

public class Test {
    static class Food {}
    static class Fruit extends Food {}
    static class Apple extends Fruit {}

    public static void main(String[] args) throws IOException {
        List<? super Fruit> fruits = new ArrayList<>();
        fruits.add(new Food());     // compile error
        fruits.add(new Fruit());    // compile success
        fruits.add(new Apple());    // compile success

        fruits = new ArrayList<Fruit>(); // compile success
        fruits = new ArrayList<Apple>(); // compile error
        fruits = new ArrayList<Food>(); // compile success
        fruits = new ArrayList<? super Fruit>(); // compile error: 通配符類型無法實例化      

        Fruit object = fruits.get(0); // compile error
    }
}

看上面代碼可知,super 通配符類型同樣不能實例化,F(xiàn)ruit 和其超類的集合均可賦值。這里 add 時 Fruit 及其子類均可成功,為啥呢?因為已知 fruits 的參數(shù)化類型必定是 Fruit 或其超類 T,那么 Fruit 及其子類肯定可以賦值給 T。

歸根到底,還是“子類對象可以賦值給超類引用,而反過來不行”這一規(guī)則導(dǎo)致 extends 和 super 通配符在 add 操作上表現(xiàn)如此的不同。同樣地,也導(dǎo)致 super 限定的 fruits 中 get 到的元素不能賦值給 Fruit 引用,而 extends 則可以。

總結(jié)一下就是:

  1. extends 可用于的返回類型限定,不能用于參數(shù)類型限定。
  2. super 可用于參數(shù)類型限定,不能用于返回類型限定。
  3. 帶有 super 超類型限定的通配符可以向泛型對易用寫入,帶有 extends 子類型限定的通配符可以向泛型對象讀取。

運行時泛型參數(shù)類型獲取

雖然 Java 的泛型在編譯期間有類型擦除,但是如果真的需要在運行時知道泛型參數(shù)的類型,應(yīng)該如何做呢?

額外保存參數(shù)類型

在上面“類型擦除”中提到了 Jackson 反序列化泛型類型,將參數(shù)類型信息顯式保存下來。

public class TestJackson {
    public static final ObjectMapper mapper = new ObjectMapper();
    public static void main(String[] args) throws IOException {
        JavaType javaType = getCollectionType(ArrayList.class, String.class);
        List<String> lst = mapper.readValue("[\"hello\",\"world\"]", javaType);
        System.out.println(lst);
    }
    // 獲取泛型的Collection Type
    public static JavaType getCollectionType(Class<?> collectionClass, Class<?>... elementClasses) {
        return mapper.getTypeFactory().constructParametricType(collectionClass, elementClasses);
    }
}

經(jīng)過 Debug 發(fā)現(xiàn),getCollectionType 返回的對象實際類型是 CollectionType:


Debug JavaType 信息

CollectionType 和 JavaType 之間的繼承關(guān)系,可以看下面的代碼:

public final class CollectionType extends CollectionLikeType {
}

public class CollectionLikeType extends TypeBase {
    protected final JavaType _elementType;  
}   

public abstract class TypeBase extends JavaType implements JsonSerializable {
    protected final JavaType _superClass;
    protected final JavaType[] _superInterfaces;
    protected final TypeBindings _bindings;
}    

注解處理器

我們可以使用注解處理器,在編譯期間獲取泛型真實類型,并保存到類文件中,詳見 Java 注解:注解處理器獲取泛型真實類型。

這個方法的本質(zhì)也是“額外保存參數(shù)類型”,只不過方法不同罷了。

signature 屬性

Java泛型的擦除并不是對所有使用泛型的地方都會擦除的,部分地方會保留泛型信息。比如 java.lang.reflect.Field 類中有一個 signature 屬性保存了泛型的參數(shù)類型信息,通過 Field 的 getGenericType 方法即可得到。當然,這種方法僅限于類中的 屬性,對于方法中的局部變量無能為力。

public final class Field extends AccessibleObject implements Member {
    private transient String    signature;
    private String getGenericSignature() {return signature;}
    public Type getGenericType() {
        if (getGenericSignature() != null)
            return getGenericInfo().getGenericType();
        else
            return getType();
    }
}

運行時能夠獲取泛型參數(shù)類型,根源在于字節(jié)碼中還是包含了這些信息的,對于下面這樣一個類:

public class Pojo {
    private String str;
    private List<Integer> intList;
    private int i;
}

使用 javac Pojo.java 命令編譯之后,使用 javap -verbose Pojo.class 命令查看其字節(jié)碼信息,可以看到常量池中,緊跟 intList 屬性存儲的就是其 Signature。

Constant pool:
   #1 = Methodref          #3.#21         // java/lang/Object."<init>":()V
   #2 = Class              #22            // Pojo
   #3 = Class              #23            // java/lang/Object
   #4 = Utf8               str
   #5 = Utf8               Ljava/lang/String;
   #6 = Utf8               intList
   #7 = Utf8               Ljava/util/List;
   #8 = Utf8               Signature
   #9 = Utf8               Ljava/util/List<Ljava/lang/Integer;>;
  #10 = Utf8               i
  #11 = Utf8               I

Field 可以獲取到泛型參數(shù)信息,類似地 Class 也是可以的。下面直接上代碼看如何獲取吧。

示例: Field

public class Pojo {
    private List<Integer> intList;
}
public class Test {
    public static void main(String[] args) throws NoSuchFieldException {
        Field intListField = Pojo.class.getDeclaredField("intList");
        Type genericType = intListField.getGenericType();
        Class<?> parameterType = (Class<?>) ((ParameterizedType) genericType).getActualTypeArguments()[0];
        System.out.println(parameterType);
    }
}

執(zhí)行以后,輸出:

class java.lang.Integer

示例:Class

public abstract class AbsClass<T> {
    protected final Type _type;
    public AbsClass() {
        Type superClass = getClass().getGenericSuperclass();
        _type = ((ParameterizedType) superClass).getActualTypeArguments()[0];
    }
    public Type getParameterizeType() {
        return _type;
    }
}
public class ParaClass extends AbsClass<Long> {
}
public class Test {
    public static void main(String[] args) throws NoSuchFieldException {
        ParaClass paraClass = new ParaClass();
        System.out.println(paraClass.getParameterizeType());
    }
}

執(zhí)行以后,輸出:

class java.lang.Long

這里 ParaClass 繼承的是 AbsClass< Long>,而非 AbsClass< T>。于是,對 ParaClass.class 調(diào)用 getGenericSuperclass(),就可以進一步獲取到 T 所綁定的 Long 類型。

有木有發(fā)現(xiàn),這兩個示例的共同點是,都用到了 ParameterizedType.getActualTypeArguments()[0] 這一句,因為泛型的參數(shù)類型也就是存在了這里。

參考文獻

  1. 泛型的使用
  2. Java 深度歷險(五)— Java 泛型
  3. Java 理論和實踐:了解泛型
  4. Java 字節(jié)碼詳解
  5. Java 為什么要添加運行時獲取泛型的方法
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

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

  • 簡介 泛型的意思就是參數(shù)化類型,通過使用參數(shù)化類型創(chuàng)建的接口、類、方法,可以指定所操作的數(shù)據(jù)類型。比如:可以使用參...
    零度沸騰_yjz閱讀 3,400評論 1 15
  • 本文大量參考Thinking in java(解析,填充)。 定義:多態(tài)算是一種泛化機制,解決了一部分可以應(yīng)用于多...
    谷歌清潔工閱讀 519評論 0 2
  • 接著上封信的內(nèi)容,談到巴菲特的投資理念。關(guān)于價值投資,就是要尋找有發(fā)展前景的公司。不過在讀了鄧普頓的逆向投資后,我...
    鹿鹿無畏閱讀 641評論 0 49
  • 與你交談,談何容易!與你相約,面孔給了誰? 我們的見面與交談怎會變的如此陌生而熟悉,叫人驚嘆! 時光匆匆,30歲,...
    關(guān)于說happy閱讀 360評論 0 0

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