java泛型那些事

泛型的類型安全性

有許多原因促成了泛型的出現(xiàn),而最引人注意的一個原因,就是為了創(chuàng)建容器類。

如果沒有泛型,如果我們需要實(shí)現(xiàn)一個通用的隊(duì)列,那么只能使用Obejct數(shù)組去實(shí)現(xiàn),并且add方法的參數(shù)和get方法的返回值都為Object:

public class MyList {
    private Object[] mData;

    public void add(Object obj) {
        ...
    }

    public Object get(int index) {
        ...
    }
    ...
}

但是這樣的話其實(shí)是很不安全的,類型安全需要靠用戶去自己維護(hù)。但用戶往往都是愚蠢的:

MyList myList = new MyList();
myList.add("1");
myList.add("2");
myList.add(3);

String val1 = (String) myList.get(0);
String val2 = (String) myList.get(1);
String val3 = (String) myList.get(2);

上面的代碼在編譯的時(shí)候沒有問題,但是真正運(yùn)行的時(shí)候程序跑著跑著就掛了,這就叫做類型不安全的設(shè)計(jì)。

使用泛型的意義在于它是類型安全的,如果使用泛型規(guī)定了參數(shù)和返回值的類型的話,上面的代碼在編譯的時(shí)候就會失敗:

public class MyList<E> {
  private Object[] mData;

  ...

  public void add(E obj) {
    ...
  }

  public E get(int index) {
    ...
    return (E) mData[index];
  }
}

MyList<String> myList = new MyList<>();
myList.add("1");
myList.add("2");
myList.add(3); //這里會編譯失敗

String val1 = myList.get(0);
String val2 = myList.get(1);
String val3 = myList.get(0);

類型標(biāo)識符

在MyList<E>聲明尖括號里面的就是類型標(biāo)識符,它其實(shí)是一個占位符,代表了某個類型,我們在類里面就能用這個占位符代表某種類型。例如add方法的參數(shù)或者get的返回值,當(dāng)然也能用來聲明一個成員變量。

可能有人會說經(jīng)??吹蕉际怯肨泛型作為泛型標(biāo)識符,為什么這里我們用E呢?

其實(shí)用什么字母做標(biāo)識符在java里面并沒有硬性規(guī)定,甚至你也可以不用僅一個字符,用一個單詞也是可以的。

不過我們通常會按照習(xí)慣在不同場景下用不同的字母標(biāo)識符:

  • E - Element (在集合中使用)
  • T - Type(Java 類)
  • K - Key(鍵)
  • V - Value(值)

泛型通配符

在泛型中有個很重要的知識點(diǎn)就是泛型類型之間是不具有繼承關(guān)系的,也就是說List<Object>并不是List<String>的父類:

public void printList(List<Object> list) {
    for (int i = 0; i < list.size(); i++) {
        System.out.println(list.get(i));
    }
}


List<String> strList = Arrays.asList("a", "b", "c", "d", "e");
printList(strList); //錯誤,List<Object>不是List<String>的父類

為了實(shí)現(xiàn)上面的printList方法,類型通配符就出現(xiàn)了:

public void printList(List<?> list) {
    for (int i = 0; i < list.size(); i++) {
        System.out.println(list.get(i));
    }
}

List<String> strList = Arrays.asList("a", "b", "c", "d", "e");
printList(strList);

List<?>可以匹配List<String>、List<Integer>等等的各種類型。

大家有可能會聽過類型通配符上限和下限,這兩個東西是怎樣的概念呢?有時(shí)候我們會需要限定只能傳入某些型的子類或者父類的容器:

  • 上限:<? extends T> 只能匹配T和T的子類

  • 下限:<? super T> 只能匹配T和T的父類

//只能傳入ClassA的子類的容器
public void printList(List<? extends ClassA> list) {
    for (int i = 0; i < list.size(); i++) {
        System.out.println(list.get(i));
    }
}

//只能傳入ClassA的父類的容器
public void printList(List<? super ClassA> list) {
    for (int i = 0; i < list.size(); i++) {
        System.out.println(list.get(i));
    }
}

除了上面的通配符"?",我們還可以直接用泛型方法去實(shí)現(xiàn)printListde,可以指定所有類型的列表或者ClassA的子類的列表:

public <T> void printList(List<T> list) {
    for (int i = 0; i < list.size(); i++) {
        System.out.println(list.get(i));
    }
}

public <T extends ClassA> void printList(List<T> list) {
    for (int i = 0; i < list.size(); i++) {
        System.out.println(list.get(i));
    }
}

當(dāng)然我們也能使用泛型的方式直接指定參數(shù)的上限,比如下面的foo方法就只能接收Number的子類:

public <T extends Number> void foo(T arg){
    ...
}

但是如果直接使用泛型的方式的話我們不能指定指定它的下限,例如下面兩種寫法都是不能通過編譯的:

//錯誤.不能直接指定泛型的下限
public <T super Number> void printList(List<T> list) {
    ...
}

//錯誤.不能直接指定泛型的下限
public <T super Number> void foo(T arg){
    ...
}

類型擦除

可能很多同學(xué)都會聽說過泛型類型擦除的概念,這個類型擦除具體指的是怎樣一回事?

可以看看下面的foo方法,它本來想實(shí)現(xiàn)的功能是:如果傳入的參數(shù)非空,就將它返回。否則,就創(chuàng)建一個同類型的實(shí)例返回。但是這段代碼是不能通過編譯的:

//錯誤,泛型的類型被擦除了,不能直接new出來
public <T> void foo(T arg) {
    return arg != null ? arg : new T();
}

原因在于java的泛型實(shí)現(xiàn)中有個叫做類型擦除的機(jī)制。簡單來講就是運(yùn)行的時(shí)候是無法獲取到泛型使用的實(shí)際類型的。

例如上面的T類型,因?yàn)槲覀冊谶\(yùn)行時(shí)不能知道它到底是什么類型,所以也就無法將它new出來。

java代碼生成的Java字節(jié)代碼中是不包含泛型中的類型信息的,所有泛型類的類型參數(shù)在編譯時(shí)都會被擦除。虛擬機(jī)中沒有泛型,只有普通類和普通方法。因此泛型的類型安全是在編譯的時(shí)候去檢測的。

所以我們創(chuàng)建泛型對象時(shí)需要指明類型,讓編譯器盡早的做參數(shù)檢查。

像下面的代碼可以順利通過,甚至可以正常運(yùn)行,直到將獲取到的數(shù)值類型的數(shù)據(jù)強(qiáng)轉(zhuǎn)成字符串的時(shí)候才報(bào)ClassCastException異常:

List list = new ArrayList<String>();
list.add("abc");
list.add(123);
String elemt1 = (String) list.get(0);
String elemt2 = (String) list.get(1); // java.lang.ClassCastException: java.lang.Integer cannot be cast to java.lang.String

我們可以用反射的方法的驗(yàn)證一下類型擦除:

List<Integer> list = new ArrayList<Integer>();
System.out.println("type : " + Arrays.toString(list.getClass().getTypeParameters()));

它得到的類型僅僅是一個占位符而已:

type : [E]

類型擦除機(jī)制的歷史原因

有人會問,為什么java會在編譯的時(shí)候?qū)㈩愋筒脸?而不像c++一樣通過在編譯的時(shí)候?qū)⒎盒皖悓?shí)例化為多個具體的類去實(shí)現(xiàn)泛型呢?

其實(shí)“實(shí)例化為多個具體的類”這樣的實(shí)現(xiàn)方式也是比較容易實(shí)現(xiàn)的,但是為了保持兼容性,所以java在泛型的實(shí)現(xiàn)上選取類型擦除的方式。實(shí)際上是做了一定的取舍的。

為什么說選用類型擦除是為了保持兼容性呢?因?yàn)榉盒筒⒉皇莏ava與生俱來的。實(shí)際上到了java5的時(shí)候才引入了泛型。

要讓以前編譯的程序在新版本的JRE還能正常運(yùn)行,就意味著以前沒有的限制不能突然冒出來。

例如在泛型出來之前java就已經(jīng)有了容器的存在,而且它具有可以存儲不同類型的的特性:

ArrayList things = new ArrayList();
things.add(Integer.valueOf(123));
things.add("abc");

那么這段代碼在Java 5引入泛型之后還必須要繼續(xù)可以運(yùn)行。

這里有兩種設(shè)計(jì)思路:

  1. 需要泛型化的類型(主要是容器(Collections)類型),以前有的就保持不變,然后平行地加一套泛型化版本的新類型;

  2. 直接把已有的類型泛型化,讓所有需要泛型化的已有類型都原地泛型化,不添加任何平行于已有類型的泛型版。

.NET在1.1 -> 2.0的時(shí)候選擇了上面選項(xiàng)的1,而Java則選擇了2。

從Java設(shè)計(jì)者的角度看,這個取舍很明白:.NET在1.1 -> 2.0的時(shí)候,實(shí)際的應(yīng)用代碼量還很少(相對Java來說),而且整個體系都在微軟的控制下,要做變更比較容易;

而在Java 1.4.2 -> 5.0的時(shí)候,Java已經(jīng)有大量程序部署在生產(chǎn)環(huán)境中,已經(jīng)有很多應(yīng)用和庫程序的代碼。如果這些代碼在新版本的Java中,為了使用Java的新功能(例如泛型)而必須做大量源碼層修改,那么新功能的普及速度就會大受影響,而且新功能會被吐槽。

在原地泛型化后,java.util.ArrayList這個類型變成了java.util.ArrayList<E>。但是以前的代碼直接用ArrayList,在新版本里必須還能繼續(xù)用,所以就引出了“raw type”的概念——一個類型雖然被泛型化了,但還可以把它當(dāng)作非泛型化的類型用。

ArrayList         - raw type
ArrayList<E>      - open generic type (assuming E is type variable)
ArrayList<String> - closed generic type
ArrayList<?>      - unbounded wildcard type

下面這樣的代碼必須可以編譯運(yùn)行:

ArrayList<Integer> ilist = new ArrayList<Integer>();
ArrayList<String> slist = new ArrayList<String>();
ArrayList list; // raw type
list = ilist;   // assigning closed generic type to raw type
list = slist;   // ditto

所以java的設(shè)計(jì)者在考慮了這一點(diǎn)之后選用類型擦除也就顯而易見了。類型擦除實(shí)際上是將泛型類型轉(zhuǎn)換了Obejct。由于所有的java類都是Object的子類,所以實(shí)現(xiàn)起來就很簡單了。只需要在編譯的時(shí)候?qū)⑺械姆盒驼嘉环紦Q成Object就可以了:

//源碼的泛型代碼
public <T> void foo(T arg){
    ...
}

//編譯時(shí)轉(zhuǎn)換成的代碼
public void foo(Object arg){
    ...
}

而在擦除類型的同時(shí),java編譯器會對該方法的調(diào)用進(jìn)行類型檢查,防止非法類型的調(diào)用。

但如果在編寫代碼的時(shí)候就已經(jīng)用raw type的話,編譯器就不會做類型的安全性檢查了。

這樣的實(shí)現(xiàn)導(dǎo)致了一個問題,List<E>泛型參數(shù)E被擦除后就變成了Object,那么就不能在泛型中使用int、long等原生數(shù)據(jù)類型了,因?yàn)樗鼈儾⒉皇荗bject的子類。

據(jù)說當(dāng)時(shí)設(shè)計(jì)java語言的程序員和產(chǎn)品經(jīng)理打了一架,并且在打贏之后成功勸服產(chǎn)品經(jīng)理在提出兼容性這樣奇葩的需求之后做出一點(diǎn)小小的讓步。(雖然只是我胡說八道的腦補(bǔ),但誰知道當(dāng)時(shí)的實(shí)際情形是不是這樣的呢?)

于是乎我們現(xiàn)在在泛型中只能使用Integer、Long等封箱類型而不能用int、long等原生類型了。

ps: 上面這段類型擦除機(jī)制的歷史原因參考了RednaxelaFX大神知乎上的一個回答,有興趣的同學(xué)可以去知乎看看原來的完整回答

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

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

  • 泛型是Java 1.5引入的新特性。泛型的本質(zhì)是參數(shù)化類型,這種參數(shù)類型可以用在類、變量、接口和方法的創(chuàng)建中,分別...
    何時(shí)不晚閱讀 3,121評論 0 2
  • 開發(fā)人員在使用泛型的時(shí)候,很容易根據(jù)自己的直覺而犯一些錯誤。比如一個方法如果接收List作為形式參數(shù),那么如果嘗試...
    時(shí)待吾閱讀 1,128評論 0 3
  • 附上思維導(dǎo)圖。這篇博客主要講了如下知識點(diǎn)。 看完了《Thinking in Java》的第十五章泛型,著實(shí)被震了一...
    Happioo閱讀 1,163評論 0 1
  • 周日的早上一早起來被所謂的喊話前男友的視頻刷屏,因?yàn)閹啄昵氨粋^,不自覺地打開了視頻??吹揭曨l里面女孩子看似鼓起勇...
    溫暖的野人閱讀 319評論 0 2
  • 國慶節(jié)回家,媽媽跟我嘮家常的時(shí)候說起了我們同村的我的一個小學(xué)同學(xué),從我媽的話中聽到他的近況,大學(xué)畢業(yè)后沒什...
    gushi84閱讀 336評論 0 0

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