深入理解 Java 泛型

[TOC]

深入理解 Java 泛型

概述

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

類型擦除

Java 中的泛型是在編譯器這個(gè)層次實(shí)現(xiàn)的,在生成的Java字節(jié)代碼中是不包含泛型中的類型信息的蜂奸。使用泛型的時(shí)候加上的類型參數(shù)犁苏,會(huì)被編譯器在編譯的時(shí)候去掉。這個(gè)過程就稱為類型擦除 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());
    }
}

上面這一段代碼围详,運(yùn)行后輸出如下,可知在運(yùn)行時(shí)獲取的類型信息是不帶具體類型的:

java.util.ArrayList
java.util.ArrayList

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

泛型類并沒有自己獨(dú)有的Class類對象助赞,只有List.class买羞。
運(yùn)行時(shí)無法獲得泛型的真實(shí)類型信息。

比如在 反序列化 Json 串至 List 字符串時(shí)雹食,需要這么做:

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。這就相當(dāng)于把 List 的元素類型 String.class 作為參數(shù)群叶,提供給了 Jackson 去反序列化吃挑。而下面的做法會(huì)編譯失敗:

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); // 編譯錯(cuò)誤
        System.out.println(lst);
    }
}

泛型不是協(xié)變的

在 Java 語言中盖呼,數(shù)組是協(xié)變的儒鹿,也就是說,如果 Integer 擴(kuò)展了 Number几晤,那么不僅 Integer 是 Number约炎,而且 Integer[] 也是 Number[],在要求 Number[] 的地方完全可以傳遞或者賦予 Integer[]蟹瘾。(更正式地說圾浅,如果 Number是 Integer 的超類型,那么 Number[] 也是 Integer[]的超類型)憾朴。您也許認(rèn)為這一原理同樣適用于泛型類型 —— List< Number> 是 List< Integer> 的超類型狸捕,那么可以在需要 List< Number> 的地方傳遞 List< Integer>。不幸的是众雷,情況并非如此灸拍。為啥呢?這么做將破壞要提供的類型安全泛型砾省。

對于數(shù)組來說鸡岗,下面的代碼會(huì)有運(yùn)行時(shí)錯(cuò)誤:

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

        objArray[0] = 1;// 運(yùn)行時(shí)錯(cuò)誤:Exception in thread "main" java.lang.ArrayStoreException: java.lang.Integer  
    }
}

而集合這么寫就會(huì)有編譯錯(cuò)誤:

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é)變的另一個(gè)后果是,不能實(shí)例化泛型類型的數(shù)組(new List< String>[3] 是不合法的)编兄,除非類型參數(shù)是一個(gè)未綁定的通配符(new List< ?>[3]是合法的)轩性。具體可以運(yùn)行下面的代碼看看:

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)造延遲

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

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

您不能使用類型參數(shù)訪問構(gòu)造函數(shù)件舵,因?yàn)樵诰幾g的時(shí)候還不知道要構(gòu)造什么類卸察,因此也就不知道使用什么構(gòu)造函數(shù)。使用泛型不能表達(dá)“T必須擁有一個(gè)拷貝構(gòu)造函數(shù)(copy constructor)”(甚至一個(gè)無參數(shù)的構(gòu)造函數(shù))這類約束铅祸,因此不能使用泛型類型參數(shù)所表示的類的構(gòu)造函數(shù)坑质。

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

不能使用 clone 方法

為什么呢洪乍?因?yàn)?clone() 在 Object 中是 protected 保護(hù)訪問的眯杏,調(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ù)表示的類型數(shù)組。編譯器不知道 T 到底表示什么類型巷波,因此不能實(shí)例化 T 數(shù)組萎津。

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

那么 ArrayList 是如何存儲數(shù)據(jù)的呢?請看下面的源代碼抹镊,是用 Object 數(shù)組存儲的锉屈,所以在獲取元素時(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: 通配符類型無法實(shí)例化  

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

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

除了 extends 還有一個(gè)通配符 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: 通配符類型無法實(shí)例化      

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

看上面代碼可知晶衷,super 通配符類型同樣不能實(shí)例化蓝纲,F(xiàn)ruit 和其超類的集合均可賦值阴孟。這里 add 時(shí) Fruit 及其子類均可成功,為啥呢税迷?因?yàn)橐阎?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 子類型限定的通配符可以向泛型對象讀取蛙奖。

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

雖然 Java 的泛型在編譯期間有類型擦除,但是如果真的需要在運(yùn)行時(shí)知道泛型參數(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 返回的對象實(shí)際類型是 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;
}    

注解處理器

我們可以使用注解處理器日裙,在編譯期間獲取泛型真實(shí)類型祭衩,并保存到類文件中,詳見 Java 注解:注解處理器獲取泛型真實(shí)類型阅签。

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

signature 屬性

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

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();
    }
}

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

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(),就可以進(jìn)一步獲取到 T 所綁定的 Long 類型阳仔。

有木有發(fā)現(xiàn)蔓彩,這兩個(gè)示例的共同點(diǎn)是,都用到了 ParameterizedType.getActualTypeArguments()[0] 這一句驳概,因?yàn)榉盒偷膮?shù)類型也就是存在了這里赤嚼。

參考文獻(xiàn)

  1. 泛型的使用
  2. Java 深度歷險(xiǎn)(五)— Java 泛型
  3. Java 理論和實(shí)踐:了解泛型
  4. Java 字節(jié)碼詳解
  5. Java 為什么要添加運(yùn)行時(shí)獲取泛型的方法
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市顺又,隨后出現(xiàn)的幾起案子更卒,更是在濱河造成了極大的恐慌,老刑警劉巖稚照,帶你破解...
    沈念sama閱讀 206,214評論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件蹂空,死亡現(xiàn)場離奇詭異,居然都是意外死亡果录,警方通過查閱死者的電腦和手機(jī)上枕,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,307評論 2 382
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來弱恒,“玉大人辨萍,你說我怎么就攤上這事》档” “怎么了锈玉?”我有些...
    開封第一講書人閱讀 152,543評論 0 341
  • 文/不壞的土叔 我叫張陵,是天一觀的道長义起。 經(jīng)常有香客問我拉背,道長,這世上最難降的妖魔是什么默终? 我笑而不...
    開封第一講書人閱讀 55,221評論 1 279
  • 正文 為了忘掉前任椅棺,我火速辦了婚禮,結(jié)果婚禮上齐蔽,老公的妹妹穿的比我還像新娘两疚。我一直安慰自己,他們只是感情好肴熏,可當(dāng)我...
    茶點(diǎn)故事閱讀 64,224評論 5 371
  • 文/花漫 我一把揭開白布鬼雀。 她就那樣靜靜地躺著顷窒,像睡著了一般蛙吏。 火紅的嫁衣襯著肌膚如雪源哩。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 49,007評論 1 284
  • 那天鸦做,我揣著相機(jī)與錄音励烦,去河邊找鬼。 笑死泼诱,一個(gè)胖子當(dāng)著我的面吹牛坛掠,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播治筒,決...
    沈念sama閱讀 38,313評論 3 399
  • 文/蒼蘭香墨 我猛地睜開眼屉栓,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了耸袜?” 一聲冷哼從身側(cè)響起友多,我...
    開封第一講書人閱讀 36,956評論 0 259
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎堤框,沒想到半個(gè)月后域滥,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 43,441評論 1 300
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡蜈抓,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 35,925評論 2 323
  • 正文 我和宋清朗相戀三年启绰,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片沟使。...
    茶點(diǎn)故事閱讀 38,018評論 1 333
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡委可,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出腊嗡,到底是詐尸還是另有隱情撤缴,我是刑警寧澤,帶...
    沈念sama閱讀 33,685評論 4 322
  • 正文 年R本政府宣布叽唱,位于F島的核電站屈呕,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏棺亭。R本人自食惡果不足惜虎眨,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,234評論 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望镶摘。 院中可真熱鬧嗽桩,春花似錦、人聲如沸凄敢。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,240評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽涝缝。三九已至扑庞,卻和暖如春譬重,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背罐氨。 一陣腳步聲響...
    開封第一講書人閱讀 31,464評論 1 261
  • 我被黑心中介騙來泰國打工臀规, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人栅隐。 一個(gè)月前我還...
    沈念sama閱讀 45,467評論 2 352
  • 正文 我出身青樓塔嬉,卻偏偏與公主長得像,于是被迫代替她去往敵國和親租悄。 傳聞我的和親對象是個(gè)殘疾皇子谨究,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 42,762評論 2 345

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

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