1、Java泛型的實現方法:類型擦除
前面已經說了,Java的泛型是僞泛型。爲何說Java的泛型是僞泛型呢?由於,在編譯期間,全部的泛型信息都會被擦除掉。正確理解泛型概念的首要前提是理解類型擦出(type erasure)。java
Java中的泛型基本上都是在編譯器這個層次來實現的。在生成的Java字節碼中是不包含泛型中的類型信息的。使用泛型的時候加上的類型參數,會在編譯器在編譯的時候去掉。這個過程就稱爲類型擦除。
編程
如在代碼中定義的List<object>和List<String>等類型,在編譯後都會編程List。JVM看到的只是List,而由泛型附加的類型信息對JVM來講是不可見的。Java編譯器會在編譯時儘量的發現可能出錯的地方,可是仍然沒法避免在運行時刻出現類型轉換異常的狀況。類型擦除也是Java的泛型實現方法與C++模版機制實現方式之間的重要區別。數組
能夠經過兩個簡單的例子,來證實java泛型的類型擦除。
例一、安全
- public class Test4 {
- public static void main(String[] args) {
- ArrayList<String> arrayList1=new ArrayList<String>();
- arrayList1.add("abc");
- ArrayList<Integer> arrayList2=new ArrayList<Integer>();
- arrayList2.add(123);
- System.out.println(arrayList1.getClass()==arrayList2.getClass());
- }
- }
在這個例子中,咱們定義了兩個ArrayList數組,不過一個是ArrayList<String>泛型類型,只能存儲字符串。一個是ArrayList<Integer>泛型類型,只能存儲整形。最後,咱們經過arrayList1對象和arrayList2對象的getClass方法獲取它們的類的信息,最後發現結果爲true。說明泛型類型String和Integer都被擦除掉了,只剩下了
原始類型
。
例二、eclipse
- public class Test4 {
- public static void main(String[] args) throws IllegalArgumentException, SecurityException, IllegalAccessException, InvocationTargetException, NoSuchMethodException {
- ArrayList<Integer> arrayList3=new ArrayList<Integer>();
- arrayList3.add(1);
- arrayList3.getClass().getMethod("add", Object.class).invoke(arrayList3, "asd");
- for (int i=0;i<arrayList3.size();i++) {
- System.out.println(arrayList3.get(i));
- }
- }
在程序中定義了一個ArrayList泛型類型實例化爲Integer的對象,若是直接調用add方法,那麼只能存儲整形的數據。不過當咱們利用反射調用add方法的時候,卻能夠存儲字符串。這說明了Integer泛型實例在編譯以後被擦除了,只保留了
原始類型
。
2、類型擦除後保留的原始類型
在上面,兩次提到了原始類型,什麼是原始類型?原始類型(raw type)就是擦除去了泛型信息,最後在字節碼中的類型變量的真正類型。不管什麼時候定義一個泛型類型,相應的原始類型都會被自動地提供。類型變量被擦除(crased),並使用其限定類型(無限定的變量用Object)替換。
例3:
ide
- class Pair<T> {
- private T value;
- public T getValue() {
- return value;
- }
- public void setValue(T value) {
- this.value = value;
- }
- }
Pair<T>的原始類型爲:
- class Pair {
- private Object value;
- public Object getValue() {
- return value;
- }
- public void setValue(Object value) {
- this.value = value;
- }
- }
由於在Pair<T>中,T是一個無限定的類型變量,因此用Object替換。其結果就是一個普通的類,如同泛型加入java變成語言以前已經實現的那樣。在程序中能夠包含不一樣類型的Pair,如Pair<String>或Pair<Integer>,可是,擦除類型後它們就成爲原始的Pair類型了,原始類型都是Object。
從上面的那個例2中,咱們也能夠明白ArrayList<Integer>被擦除類型後,原始類型也變成了Object,因此經過反射咱們就能夠存儲字符串了。
測試
若是類型變量有限定,那麼原始類型就用第一個邊界的類型變量來替換。this
好比Pair這樣聲明spa
例4:.net
- public class Pair<T extends Comparable& Serializable> {
那麼原始類型就是Comparable
注意:
若是Pair這樣聲明public class Pair<T extends Serializable&Comparable> ,那麼原始類型就用Serializable替換,而編譯器在必要的時要向Comparable插入強制類型轉換。爲了提升效率,應該將標籤(tagging)接口(即沒有方法的接口)放在邊界限定列表的末尾。
要區分原始類型和泛型變量的類型
在調用泛型方法的時候,能夠指定泛型,也能夠不指定泛型。
在不指定泛型的狀況下,泛型變量的類型爲 該方法中的幾種類型的同一個父類的最小級,直到Object。
在指定泛型的時候,該方法中的幾種類型必須是該泛型實例類型或者其子類。
- public class Test2{
- public static void main(String[] args) {
-
- int i=Test2.add(1, 2);
- Number f=Test2.add(1, 1.2);
- Object o=Test2.add(1, "asd");
-
-
- int a=Test2.<Integer>add(1, 2);
- int b=Test2.<Integer>add(1, 2.2);
- Number c=Test2.<Number>add(1, 2.2);
- }
-
-
- public static <T> T add(T x,T y){
- return y;
- }
- }
其實在泛型類中,不指定泛型的時候,也差很少,只不過這個時候的泛型類型爲Object,就好比ArrayList中,若是不指定泛型,那麼這個ArrayList中能夠聽任意類型的對象。
舉例:
- public static void main(String[] args) {
- ArrayList arrayList=new ArrayList();
- arrayList.add(1);
- arrayList.add("121");
- arrayList.add(new Date());
- }
3、類型擦除引發的問題及解決方法
由於種種緣由,Java不能實現真正的泛型,只能使用類型擦除來實現僞泛型,這樣雖然不會有類型膨脹的問題,可是也引發了許多新的問題。因此,Sun對這些問題做出了許多限制,避免咱們犯各類錯誤。
一、先檢查,在編譯,以及檢查編譯的對象和引用傳遞的問題
既然說類型變量會在編譯的時候擦除掉,那爲何咱們往ArrayList<String> arrayList=new ArrayList<String>();所建立的數組列表arrayList中,不能使用add方法添加整形呢?不是說泛型變量Integer會在編譯時候擦除變爲原始類型Object嗎,爲何不能存別的類型呢?既然類型擦除了,如何保證咱們只能使用泛型變量限定的類型呢?
java是如何解決這個問題的呢?java編譯器是經過先檢查代碼中泛型的類型,而後再進行類型擦除,在進行編譯的。
舉個例子說明:
- public static void main(String[] args) {
- ArrayList<String> arrayList=new ArrayList<String>();
- arrayList.add("123");
- arrayList.add(123);
- }
在上面的程序中,使用add方法添加一個整形,在eclipse中,直接就會報錯,說明這就是在編譯以前的檢查。由於若是是在編譯以後檢查,類型擦除後,原始類型爲Object,是應該運行任意引用類型的添加的。可實際上卻不是這樣,這偏偏說明了關於泛型變量的使用,是會在編譯以前檢查的。
那麼,這麼類型檢查是針對誰的呢?咱們先看看參數化類型與原始類型的兼容
以ArrayList舉例子,之前的寫法:
- ArrayList arrayList=new ArrayList();
如今的寫法:
- ArrayList<String> arrayList=new ArrayList<String>();
若是是與之前的代碼兼容,各類引用傳值之間,必然會出現以下的狀況:
- ArrayList<String> arrayList1=new ArrayList();
- ArrayList arrayList2=new ArrayList<String>();
這樣是沒有錯誤的,不過會有個編譯時警告。
不過在第一種狀況,能夠實現與 徹底使用泛型參數同樣的效果,第二種則徹底沒效果。
由於,原本類型檢查就是編譯時完成的。new ArrayList()只是在內存中開闢一個存儲空間,能夠存儲任何的類型對象。而真正涉及類型檢查的是它的引用,由於咱們是使用它引用arrayList1 來調用它的方法,好比說調用add()方法。因此arrayList1引用能完成泛型類型的檢查。
而引用arrayList2沒有使用泛型,因此不行。
舉例子:
- public class Test10 {
- public static void main(String[] args) {
-
-
- ArrayList<String> arrayList1=new ArrayList();
- arrayList1.add("1");
- arrayList1.add(1);
- String str1=arrayList1.get(0);
-
- ArrayList arrayList2=new ArrayList<String>();
- arrayList2.add("1");
- arrayList2.add(1);
- Object object=arrayList2.get(0);
-
- new ArrayList<String>().add("11");
- new ArrayList<String>().add(22);
- String string=new ArrayList<String>().get(0);
- }
- }
經過上面的例子,咱們能夠明白,類型檢查就是針對引用的,誰是一個引用,用這個引用調用泛型方法,就會對這個引用調用的方法進行類型檢測,而無關它真正引用的對象。
從這裏,咱們能夠再討論下 泛型中參數化類型爲何不考慮繼承關係
在Java中,像下面形式的引用傳遞是不容許的:
- ArrayList<String> arrayList1=new ArrayList<Object>();
- ArrayList<Object> arrayList1=new ArrayList<String>();
咱們先看第一種狀況,將第一種狀況拓展成下面的形式:
- ArrayList<Object> arrayList1=new ArrayList<Object>();
- arrayList1.add(new Object());
- arrayList1.add(new Object());
- ArrayList<String> arrayList2=arrayList1;
實際上,在第4行代碼的時候,就會有編譯錯誤。那麼,咱們先假設它編譯沒錯。那麼當咱們使用arrayList2引用用get()方法取值的時候,返回的都是String類型的對象(上面提到了,類型檢測是根據引用來決定的。),但是它裏面實際上已經被咱們存放了Object類型的對象,這樣,就會有ClassCastException了。因此爲了不這種極易出現的錯誤,Java不容許進行這樣的引用傳遞。(這也是泛型出現的緣由,就是爲了解決類型轉換的問題,咱們不能違背它的初衷)。
在看第二種狀況,將第二種狀況拓展成下面的形式:
- ArrayList<String> arrayList1=new ArrayList<String>();
- arrayList1.add(new String());
- arrayList1.add(new String());
- ArrayList<Object> arrayList2=arrayList1;
沒錯,這樣的狀況比第一種狀況好的多,最起碼,在咱們用arrayList2取值的時候不會出現ClassCastException,由於是從String轉換爲Object。但是,這樣作有什麼意義呢,泛型出現的緣由,就是爲了解決類型轉換的問題。咱們使用了泛型,到頭來,仍是要本身強轉,違背了泛型設計的初衷。因此java不容許這麼幹。再說,你若是又用arrayList2往裏面add()新的對象,那麼到時候取得時候,我怎麼知道我取出來的究竟是String類型的,仍是Object類型的呢?
因此,要格外注意,泛型中的引用傳遞的問題。
二、自動類型轉換
由於類型擦除的問題,因此全部的泛型類型變量最後都會被替換爲原始類型。這樣就引發了一個問題,既然都被替換爲原始類型,那麼爲何咱們在獲取的時候,不須要進行強制類型轉換呢?看下ArrayList和get方法:
- public E get(int index) {
- RangeCheck(index);
- return (E) elementData[index];
- }
看以看到,在return以前,會根據泛型變量進行強轉。假設泛型類型變量爲Date,雖然泛型信息會被擦除掉,可是會將(E) elementData[index],編譯爲(Date)elementData[index]。因此咱們不用本身進行強轉。
當存取一個泛型域時也會自動插入強制類型轉換。假設Pair類的value域是public的,那麼,表達式:
也會自動地在結果字節碼中插入強制類型轉換。
三、類型擦除與多態的衝突和解決方法
如今有這樣一個泛型類:
- class Pair<T> {
- private T value;
- public T getValue() {
- return value;
- }
- public void setValue(T value) {
- this.value = value;
- }
- }
而後咱們想要一個子類繼承它
- class DateInter extends Pair<Date> {
- @Override
- public void setValue(Date value) {
- super.setValue(value);
- }
- @Override
- public Date getValue() {
- return super.getValue();
- }
- }
在這個子類中,咱們設定父類的泛型類型爲Pair<Date>,在子類中,咱們覆蓋了父類的兩個方法,咱們的原意是這樣的:
將父類的泛型類型限定爲Date,那麼父類裏面的兩個方法的參數都爲Date類型:「
- public Date getValue() {
- return value;
- }
- public void setValue(Date value) {
- this.value = value;
- }
因此,咱們在子類中重寫這兩個方法一點問題也沒有,實際上,從他們的@Override標籤中也能夠看到,一點問題也沒有,其實是這樣的嗎?
分析:
實際上,類型擦除後,父類的的泛型類型所有變爲了原始類型Object,因此父類編譯以後會變成下面的樣子:
- class Pair {
- private Object value;
- public Object getValue() {
- return value;
- }
- public void setValue(Object value) {
- this.value = value;
- }
- }
再看子類的兩個重寫的方法的類型:
- @Override
- public void setValue(Date value) {
- super.setValue(value);
- }
- @Override
- public Date getValue() {
- return super.getValue();
- }
先來分析setValue方法,父類的類型是Object,而子類的類型是Date,參數類型不同,這若是實在普通的繼承關係中,根本就不會是重寫,而是重載。
咱們在一個main方法測試一下:
- public static void main(String[] args) throws ClassNotFoundException {
- DateInter dateInter=new DateInter();
- dateInter.setValue(new Date());
- dateInter.setValue(new Object());
- }
若是是重載,那麼子類中兩個setValue方法,一個是參數Object類型,一個是Date類型,但是咱們發現,根本就沒有這樣的一個子類繼承自父類的Object類型參數的方法。因此說,倒是是重寫了,而不是重載了。
爲何會這樣呢?
緣由是這樣的,咱們傳入父類的泛型類型是Date,Pair<Date>,咱們的本意是將泛型類變爲以下:
- class Pair {
- private Date value;
- public Date getValue() {
- return value;
- }
- public void setValue(Date value) {
- this.value = value;
- }
- }
而後再子類中重寫參數類型爲Date的那兩個方法,實現繼承中的多態。
但是因爲種種緣由,虛擬機並不能將泛型類型變爲Date,只能將類型擦除掉,變爲原始類型Object。這樣,咱們的本意是進行重寫,實現多態。但是類型擦除後,只能變爲了重載。這樣,類型擦除就和多態有了衝突。JVM知道你的本意嗎?知道!!!但是它能直接實現嗎,不能!!!若是真的不能的話,那咱們怎麼去重寫咱們想要的Date類型參數的方法啊。
因而JVM採用了一個特殊的方法,來完成這項功能,那就是橋方法。
首先,咱們用javap -c className的方式反編譯下DateInter子類的字節碼,結果以下:
- class com.tao.test.DateInter extends com.tao.test.Pair<java.util.Date> {
- com.tao.test.DateInter();
- Code:
- 0: aload_0
- 1: invokespecial #8
- :()V
- 4: return
-
- public void setValue(java.util.Date);
- Code:
- 0: aload_0
- 1: aload_1
- 2: invokespecial #16
- :(Ljava/lang/Object;)V
- 5: return
-
- public java.util.Date getValue();
- Code:
- 0: aload_0
- 1: invokespecial #23
- :()Ljava/lang/Object;
- 4: checkcast #26
- 7: areturn
-
- public java.lang.Object getValue();
- Code:
- 0: aload_0
- 1: invokevirtual #28
- ;
- 4: areturn
-
- public void setValue(java.lang.Object);
- Code:
- 0: aload_0
- 1: aload_1
- 2: checkcast #26
- 5: invokevirtual #30
- )V
- 8: return
- }
從編譯的結果來看,咱們本意重寫setValue和getValue方法的子類,居然有4個方法,其實不用驚奇,最後的兩個方法,就是編譯器本身生成的橋方法。能夠看到橋方法的參數類型都是Object,也就是說,子類中真正覆蓋父類兩個方法的就是這兩個咱們看不到的橋方法。而打在咱們本身定義的setvalue和getValue方法上面的@Oveerride只不過是假象。而橋方法的內部實現,就只是去調用咱們本身重寫的那兩個方法。
因此,虛擬機巧妙的使用了巧方法,來解決了類型擦除和多態的衝突。
不過,要提到一點,這裏面的setValue和getValue這兩個橋方法的意義又有不一樣。
setValue方法是爲了解決類型擦除與多態之間的衝突。
而getValue卻有廣泛的意義,怎麼說呢,若是這是一個普通的繼承關係:
那麼父類的setValue方法以下:
- public ObjectgetValue() {
- return super.getValue();
- }
而子類重寫的方法是:
- public Date getValue() {
- return super.getValue();
- }
其實這在普通的類繼承中也是廣泛存在的重寫,這就是協變。
關於協變:。。。。。。
而且,還有一點也許會有疑問,子類中的巧方法 Object getValue()和Date getValue()是同 時存在的,但是若是是常規的兩個方法,他們的方法簽名是同樣的,也就是說虛擬機根本不能分別這兩個方法。若是是咱們本身編寫Java代碼,這樣的代碼是沒法經過編譯器的檢查的,可是虛擬機倒是容許這樣作的,由於虛擬機經過參數類型和返回類型來肯定一個方法,因此編譯器爲了實現泛型的多態容許本身作這個看起來「不合法」的事情,而後交給虛擬器去區別。
四、泛型類型變量不能是基本數據類型
不能用類型參數替換基本類型。就好比,沒有ArrayList<double>,只有ArrayList<Double>。由於當類型擦除後,ArrayList的原始類型變爲Object,可是Object類型不能存儲double值,只能引用Double的值。
五、運行時類型查詢
舉個例子:
- ArrayList<String> arrayList=new ArrayList<String>();
由於類型擦除以後,ArrayList<String>只剩下原始類型,泛型信息String不存在了。
那麼,運行時進行類型查詢的時候使用下面的方法是錯誤的
java限定了這種類型查詢的方式
- if( arrayList instanceof ArrayList<?>)
? 是通配符的形式 ,將在後面一篇中介紹。
六、異常中使用泛型的問題
一、不能拋出也不能捕獲泛型類的對象。事實上,泛型類擴展Throwable都不合法。例如:下面的定義將不會經過編譯:
- public class Problem<T> extends Exception{......}
爲何不能擴展Throwable,由於異常都是在運行時捕獲和拋出的,而在編譯的時候,泛型信息全都會被擦除掉,那麼,假設上面的編譯可行,那麼,在看下面的定義:
- try{
- }catch(Problem<Integer> e1){
- 。。
- }catch(Problem<Number> e2){
- ...
- }
類型信息被擦除後,那麼兩個地方的catch都變爲原始類型Object,那麼也就是說,這兩個地方的catch變的如出一轍,就至關於下面的這樣
- try{
- }catch(Problem<Object> e1){
- 。。
- }catch(Problem<Object> e2){
- ...
這個固然就是不行的。就比如,catch兩個如出一轍的普通異常,不能經過編譯同樣:
- try{
- }catch(Exception e1){
- 。。
- }catch(Exception e2){
- ...
二、不能再catch子句中使用泛型變量
- public static <T extends Throwable> void doWork(Class<T> t){
- try{
- ...
- }catch(T e){
- ...
- }
- }
由於泛型信息在編譯的時候已經變味原始類型,也就是說上面的T會變爲原始類型Throwable,那麼若是能夠再catch子句中使用泛型變量,那麼,下面的定義呢:
- public static <T extends Throwable> void doWork(Class<T> t){
- try{
- ...
- }catch(T e){
- ...
- }catch(IndexOutOfBounds e){
- }
- }
根據異常捕獲的原則,必定是子類在前面,父類在後面,那麼上面就違背了這個原則。即便你在使用該靜態方法的使用T是ArrayIndexOutofBounds,在編譯以後仍是會變成Throwable,ArrayIndexOutofBounds是IndexOutofBounds的子類,違背了異常捕獲的原則。因此java爲了不這樣的狀況,禁止在catch子句中使用泛型變量。
可是在異常聲明中可使用類型變量。下面方法是合法的。
- public static<T extends Throwable> void doWork(T t) throws T{
- try{
- ...
- }catch(Throwable realCause){
- t.initCause(realCause);
- throw t;
- }
上面的這樣使用是沒問題的。
七、數組(這個不屬於類型擦除引發的問題)
不能聲明參數化類型的數組。如:
- Pair<String>[] table = newPair<String>(10);
這是由於擦除後,table的類型變爲Pair[],能夠轉化成一個Object[]。
- Object[] objarray =table;
數組能夠記住本身的元素類型,下面的賦值會拋出一個ArrayStoreException異常。
對於泛型而言,擦除下降了這個機制的效率。下面的賦值能夠經過數組存儲的檢測,但仍然會致使類型錯誤。
- objarray =new Pair<Employee>();
提示:若是須要收集參數化類型對象,直接使用ArrayList:ArrayList<Pair<String>>最安全且有效。
八、泛型類型的實例化
不能實例化泛型類型。如,
是錯誤的,類型擦除會使這個操做作成new Object()。
不能創建一個泛型數組。
- public<T> T[] minMax(T[] a){
- T[] mm = new T[2];
- ...
- }
相似的,擦除會使這個方法老是構靠一個Object[2]數組。可是,能夠用反射構造泛型對象和數組。
利用反射,調用Array.newInstance:
- publicstatic <T extends Comparable> T[]minmax(T[] a)
-
- {
-
- T[] mm == (T[])Array.newInstance(a.getClass().getComponentType(),2);
-
- ...
-
-
-
-
-
-
-
- }
九、類型擦除後的衝突
一、
當泛型類型被擦除後,建立條件不能產生衝突。若是在Pair類中添加下面的equals方法:
- class Pair<T> {
- public boolean equals(T value) {
- return null;
- }
-
- }
考慮一個Pair<String>。從概念上,它有兩個equals方法:
booleanequals(String); //在Pair<T>中定義
boolean equals(Object); //從object中繼承
可是,這只是一種錯覺。實際上,擦除後方法
boolean equals(T)
變成了方法 boolean equals(Object)
這與Object.equals方法是衝突的!固然,補救的辦法是從新命名引起錯誤的方法。
二、
泛型規範說明說起另外一個原則「要支持擦除的轉換,須要強行制一個類或者類型變量不能同時成爲兩個接口的子類,而這兩個子類是同一接品的不一樣參數化。」
下面的代碼是非法的:
- class Calendar implements Comparable<Calendar>{ ... }
- class GregorianCalendar extends Calendar implements Comparable<GregorianCalendar>{...}
GregorianCalendar會實現Comparable<Calender>和Compable<GregorianCalendar>,這是同一個接口的不一樣參數化實現。
這一限制與類型擦除的關係並不很明確。非泛型版本:
- class Calendar implements Comparable{ ... }
- class GregorianCalendar extends Calendar implements Comparable{...}
是合法的。
十、泛型在靜態方法和靜態類中的問題
泛型類中的靜態方法和靜態變量不可使用泛型類所聲明的泛型類型參數
舉例說明:
由於泛型類中的泛型參數的實例化是在定義對象的時候指定的,而靜態變量和靜態方法不須要使用對象來調用。對象都沒有建立,如何肯定這個泛型參數是何種類型,因此固然是錯誤的。
可是要注意區分下面的一種狀況:
由於這是一個泛型方法,在泛型方法中使用的T是本身在方法中定義的T,而不是泛型類中的T。