您好,登錄后才能下訂單哦!
這期內容當中小編將會給大家?guī)碛嘘Pjava泛型類型擦除的示例分析,文章內容豐富且以專業(yè)的角度為大家分析和敘述,閱讀完這篇文章希望大家可以有所收獲。
大家都知道,Java的泛型是偽泛型,這是因為Java在編譯期間,所有的泛型信息都會被擦掉,正確理解泛型概念的首要前提是理解類型擦除。Java的泛型基本上都是在編譯器這個層次上實現(xiàn)的,在生成的字節(jié)碼中是不包含泛型中的類型信息的,使用泛型的時候加上類型參數(shù),在編譯器編譯的時候會去掉,這個過程成為類型擦除。
如在代碼中定義List<Object>
和List<String>
等類型,在編譯后都會變成List
,JVM看到的只是List,而由泛型附加的類型信息對JVM是看不到的。Java編譯器會在編譯時盡可能的發(fā)現(xiàn)可能出錯的地方,但是仍然無法在運行時刻出現(xiàn)的類型轉換異常的情況,類型擦除也是Java的泛型與C++模板機制實現(xiàn)方式之間的重要區(qū)別。
例1.原始類型相等
public class Test { public static void main(String[] args) { ArrayList<String> list1 = new ArrayList<String>(); list1.add("abc"); ArrayList<Integer> list2 = new ArrayList<Integer>(); list2.add(123); System.out.println(list1.getClass() == list2.getClass()); } }
在這個例子中,我們定義了兩個ArrayList
數(shù)組,不過一個是ArrayList<String>
泛型類型的,只能存儲字符串;一個是ArrayList<Integer>
泛型類型的,只能存儲整數(shù),最后,我們通過list1
對象和list2
對象的getClass()
方法獲取他們的類的信息,最后發(fā)現(xiàn)結果為true
。說明泛型類型String
和Integer
都被擦除掉了,只剩下原始類型。
例2.通過反射添加其它類型元素
public class Test { public static void main(String[] args) throws Exception { ArrayList<Integer> list = new ArrayList<Integer>(); list.add(1); //這樣調用 add 方法只能存儲整形,因為泛型類型的實例為 Integer list.getClass().getMethod("add", Object.class).invoke(list, "asd"); for (int i = 0; i < list.size(); i++) { System.out.println(list.get(i)); } } }
在程序中定義了一個ArrayList
泛型類型實例化為Integer
對象,如果直接調用add()
方法,那么只能存儲整數(shù)數(shù)據(jù),不過當我們利用反射調用add()
方法的時候,卻可以存儲字符串,這說明了Integer
泛型實例在編譯之后被擦除掉了,只保留了原始類型。
在上面,兩次提到了原始類型,什么是原始類型?
原始類型 :就是擦除去了泛型信息,最后在字節(jié)碼中的類型變量的真正類型,無論何時定義一個泛型,相應的原始類型都會被自動提供,類型變量擦除,并使用其限定類型(無限定的變量用Object)替換。
例3.原始類型Object
class Pair<T> { private T value; public T getValue() { return value; } public void setValue(T value) { this.value = value; } }
Pair的原始類型編譯為bytecode后:
class Pair { private Object value; public Object getValue() { return value; } public void setValue(Object value) { this.value = value; } }
因為在Pair<T>
中,T 是一個無限定的類型變量,所以用Object
替換,其結果就是一個普通的類,如同泛型加入Java語言之前的已經(jīng)實現(xiàn)的樣子。在程序中可以包含不同類型的Pair
,如Pair<String>
或Pair<Integer>
,但是擦除類型后他們的就成為原始的Pair
類型了,原始類型都是Object
。主要是為了迎合JDK 1.5以前的。
從上面的例2中,我們也可以明白ArrayList<Integer>
被擦除類型后,原始類型也變?yōu)?code>Object,所以通過反射我們就可以存儲字符串了。
如果類型變量有限定,那么原始類型就用第一個邊界的類型變量類替換。
比如: Pair這樣聲明的話
public class Pair<T extends Comparable> {}
那么原始類型就是Comparable
。
要區(qū)分原始類型和泛型變量的類型。
在調用泛型方法時,可以指定泛型,也可以不指定泛型。
在不指定泛型的情況下,泛型變量的類型為該方法中的幾種類型的同一父類的最小級(繼承圖最早從下到上最早相交的地方),直到Object
在指定泛型的情況下,該方法的幾種類型必須是該泛型的實例的類型或者其子類
public class Test { public static void main(String[] args) { /**不指定泛型的時候*/ int i = Test.add(1, 2); //這兩個參數(shù)都是Integer,所以T為Integer類型 Number f = Test.add(1, 1.2); //這兩個參數(shù)一個是Integer,以風格是Float,所以取同一父類的最小級,為Number Object o = Test.add(1, "asd"); //這兩個參數(shù)一個是Integer,以風格是Float,所以取同一父類的最小級,為Object /**指定泛型的時候*/ int a = Test.<Integer>add(1, 2); //指定了Integer,所以只能為Integer類型或者其子類 int b = Test.<Integer>add(1, 2.2); //編譯錯誤,指定了Integer,不能為Float Number c = Test.<Number>add(1, 2.2); //指定為Number,所以可以為Integer和Float } //這是一個簡單的泛型方法 public static <T> T add(T x,T y){ return y; } }
其實在泛型類中,不指定泛型的時候,也差不多,只不過這個時候的泛型為Object
,就比如 ArrayList
中,如果不指定泛型,那么這個ArrayList
可以存儲任意的對象。
例4.Object泛型
public static void main(String[] args) { ArrayList list = new ArrayList(); list.add(1); list.add("121"); list.add(new Date()); }
因為種種原因,Java不能實現(xiàn)真正的泛型,只能使用類型擦除來實現(xiàn)偽泛型,這樣雖然不會有類型膨脹問題,但是也引起來許多新問題,所以,SUN對這些問題做出了種種限制,避免我們發(fā)生各種錯誤。
Q: 既然說類型變量會在編譯的時候擦除掉,那為什么我們往 ArrayList 創(chuàng)建的對象中添加整數(shù)會報錯呢?不是說泛型變量String會在編譯的時候變?yōu)镺bject類型嗎?為什么不能存別的類型呢?既然類型擦除了,如何保證我們只能使用泛型變量限定的類型呢?
例如:
public static void main(String[] args) { ArrayList<String> list = new ArrayList<String>(); list.add("123"); list.add(123);//編譯錯誤 }
在上面的程序中,使用add
方法添加一個整型,在IDE中,直接會報錯,說明這就是在編譯之前的檢查,因為如果是在編譯之后檢查,類型擦除后,原始類型為Object
,是應該允許任意引用類型添加的??蓪嶋H上卻不是這樣的,這恰恰說明了關于泛型變量的使用,是會在編譯之前檢查的。
那么,這個類型檢查是針對誰的呢?我們先看看參數(shù)化類型和原始類型的兼容。
以 ArrayList舉例子,以前的寫法:
ArrayList list = new ArrayList();
現(xiàn)在的寫法:
ArrayList<String> list = new ArrayList<String>();
如果是與以前的代碼兼容,各種引用傳值之間,必然會出現(xiàn)如下的情況:
ArrayList<String> list1 = new ArrayList(); //第一種 情況 ArrayList list2 = new ArrayList<String>(); //第二種 情況
這樣是沒有錯誤的,不過會有個編譯時警告。
不過在第一種情況,可以實現(xiàn)與完全使用泛型參數(shù)一樣的效果,第二種則沒有效果。
因為類型檢查就是編譯時完成的,new ArrayList()
只是在內存中開辟了一個存儲空間,可以存儲任何類型對象,而真正設計類型檢查的是它的引用,因為我們是使用它引用list1
來調用它的方法,比如說調用add
方法,所以list1
引用能完成泛型類型的檢查。而引用list2
沒有使用泛型,所以不行。跟進 變量聲明時候的類型進行類型檢查。
舉例子:
public class Test { public static void main(String[] args) { ArrayList<String> list1 = new ArrayList(); list1.add("1"); //編譯通過 list1.add(1); //編譯錯誤 String str1 = list1.get(0); //返回類型就是String ArrayList list2 = new ArrayList<String>(); list2.add("1"); //編譯通過 list2.add(1); //編譯通過 Object object = list2.get(0); //返回類型就是Object new ArrayList<String>().add("11"); //編譯通過 new ArrayList<String>().add(22); //編譯錯誤 String str2 = new ArrayList<String>().get(0); //返回類型就是String } }
結論超級重要:泛型中的類型檢查就是針對引用的,誰是一個引用,用這個引用調用泛型方法,就會對這個引用調用的方法進行類型檢測,而無關它真正引用的對象。
泛型中參數(shù)化類型為什么不考慮繼承關系?
在Java中,像下面形式的引用傳遞是不允許的:
ArrayList<String> list1 = new ArrayList<Object>(); //編譯錯誤 這是不同的兩個類型 ArrayList<Object> list2 = new ArrayList<String>(); //編譯錯誤 這是不同的兩個類型
我們先看第一種情況,將第一種情況拓展成下面的形式:
ArrayList<Object> list1 = new ArrayList<Object>(); list1.add(new Object()); list1.add(new Object()); ArrayList<String> list2 = list1; //編譯錯誤
實際上,在第4行代碼的時候,就會有編譯錯誤。那么,我們先假設它編譯沒錯。那么當我們使用list2
引用用get()
方法取值的時候,返回的都是String
類型的對象(上面提到了,類型檢測是根據(jù)引用來決定的),可是它里面實際上已經(jīng)被我們存放了Object
類型的對象,這樣就會有ClassCastException
了。所以為了避免這種極易出現(xiàn)的錯誤,Java不允許進行這樣的引用傳遞。(這也是泛型出現(xiàn)的原因,就是為了解決類型轉換的問題,我們不能違背它的初衷),一個是在放之前就設定放的規(guī)則(JDK1.5以后),一個是放之后取的時候再設定規(guī)則(JDK1.5之前),顯然1.5之后更好 。
再看第二種情況,將第二種情況拓展成下面的形式:
ArrayList<String> list1 = new ArrayList<String>(); list1.add(new String()); list1.add(new String()); ArrayList<Object> list2 = list1; //編譯錯誤
沒錯,這樣的情況比第一種情況好的多,最起碼,在我們用list2
取值的時候不會出現(xiàn)ClassCastException
,因為是從String
轉換為Object
??墒牵@樣做有什么意義呢,泛型出現(xiàn)的原因,就是為了解決類型轉換的問題。我們使用了泛型,到頭來,還是要自己強轉,違背了泛型設計的初衷。所以java不允許這么干。再說,你如果又用list2
往里面add()
新的對象,那么到時候取得時候,我怎么知道我取出來的到底是String
類型的,還是Object
類型的呢?
所以,要格外注意,泛型中的引用傳遞的問題。
因為類型擦除的問題,所以所有的泛型類型變量最后都會被替換為原始類型。
既然都被替換為原始類型,那么為什么我們在獲取的時候,不需要進行強制類型轉換呢?
看下ArrayList.get()
方法:
public E get(int index) { RangeCheck(index); return (E) elementData[index]; }
可以看到,在return
之前,會根據(jù)泛型變量進行強轉。假設泛型類型變量為Date
,雖然泛型信息會被擦除掉,但是會將(E) elementData[index]
,編譯為(Date)elementData[index]
。所以我們不用自己進行強轉。當存取一個泛型域時也會自動插入強制類型轉換。假設Pair
類的value
域是public
的,那么表達式:
Date date = pair.value;
也會自動地在結果字節(jié)碼中插入強制類型轉換。
現(xiàn)在有這樣一個泛型類:
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
,那么父類里面的兩個方法的參數(shù)都為Date
類型。
public Date getValue() { return value; } public void setValue(Date value) { this.value = value; }
所以,我們在子類中重寫這兩個方法一點問題也沒有,實際上,從他們的@Override
標簽中也可以看到,一點問題也沒有,實際上是這樣的嗎?
分析:實際上,類型擦除后,父類的的泛型類型全部變?yōu)榱嗽碱愋?code>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
,參數(shù)類型不一樣,這如果實在普通的繼承關系中,根本就不是重寫,而是重載。
我們在一個main方法測試一下:
public static void main(String[] args) throws ClassNotFoundException { DateInter dateInter = new DateInter(); dateInter.setValue(new Date()); dateInter.setValue(new Object()); //編譯錯誤 }
如果是重載,那么子類中兩個setValue
方法,一個是參數(shù)Object
類型,一個是Date
類型,可是我們發(fā)現(xiàn),根本就沒有這樣的一個子類繼承自父類的Object類型參數(shù)的方法。所以說,確實是重寫了,而不是重載了。
為什么會這樣呢?
原因是這樣的,我們傳入父類的泛型類型是Date,Pair<Date>
,我們的本意是將泛型類變?yōu)槿缦?/strong>:
class Pair { private Date value; public Date getValue() { return value; } public void setValue(Date value) { this.value = value; } }
然后再子類中重寫參數(shù)類型為Date的那兩個方法,實現(xiàn)繼承中的多態(tài)。
可是由于種種原因,虛擬機并不能將泛型類型變?yōu)?code>Date,只能將類型擦除掉,變?yōu)樵碱愋?code>Object。這樣,我們的本意是進行重寫,實現(xiàn)多態(tài)??墒穷愋筒脸?,只能變?yōu)榱酥剌d。這樣,類型擦除就和多態(tài)有了沖突。JVM知道你的本意嗎?知道?。。】墒撬苤苯訉崿F(xiàn)嗎,不能?。?!如果真的不能的話,那我們怎么去重寫我們想要的Date
類型參數(shù)的方法啊。
于是JVM采用了一個特殊的方法,來完成這項功能,那就是橋方法。
首先,我們用javap -c className
的方式反編譯下DateInter
子類的字節(jié)碼,結果如下:
class com.tao.test.DateInter extends com.tao.test.Pair<java.util.Date> { com.tao.test.DateInter(); Code: 0: aload_0 1: invokespecial #8 // Method com/tao/test/Pair."<init>":()V 4: return public void setValue(java.util.Date); //我們重寫的setValue方法 Code: 0: aload_0 1: aload_1 2: invokespecial #16 // Method com/tao/test/Pair.setValue:(Ljava/lang/Object;)V 5: return public java.util.Date getValue(); //我們重寫的getValue方法 Code: 0: aload_0 1: invokespecial #23 // Method com/tao/test/Pair.getValue:()Ljava/lang/Object; 4: checkcast #26 // class java/util/Date 7: areturn public java.lang.Object getValue(); //編譯時由編譯器生成的巧方法 Code: 0: aload_0 1: invokevirtual #28 // Method getValue:()Ljava/util/Date 去調用我們重寫的getValue方法; 4: areturn public void setValue(java.lang.Object); //編譯時由編譯器生成的巧方法 Code: 0: aload_0 1: aload_1 2: checkcast #26 // class java/util/Date 5: invokevirtual #30 // Method setValue:(Ljava/util/Date; 去調用我們重寫的setValue方法)V 8: return }
從編譯的結果來看,我們本意重寫setValue
和getValue
方法的子類,竟然有4個方法,其實不用驚奇,最后的兩個方法,就是編譯器自己生成的橋方法??梢钥吹綐蚍椒ǖ膮?shù)類型都是Object,也就是說,子類中真正覆蓋父類兩個方法的就是這兩個我們看不到的橋方法。而打在我們自己定義的setvalue
和getValue
方法上面的@Oveerride
只不過是假象。而橋方法的內部實現(xiàn),就只是去調用我們自己重寫的那兩個方法。
所以,虛擬機巧妙的使用了橋方法,來解決了類型擦除和多態(tài)的沖突。
不過,要提到一點,這里面的setValue
和getValue
這兩個橋方法的意義又有不同。
setValue
方法是為了解決類型擦除與多態(tài)之間的沖突。因為入?yún)⒉灰粯印?/p>
而getValue
卻有普遍的意義,怎么說呢,如果這是一個普通的繼承關系:
那么父類的setValue
方法如下:
public Object getValue() { return super.getValue(); }
而子類重寫的方法是:
public Date getValue() { return super.getValue(); }
其實這在普通的類繼承中也是普遍存在的重寫,這就是 協(xié)變。
關于協(xié)變:
并且,還有一點也許會有疑問,子類中的巧方法Object getValue()
和Date getValue()
是同 時存在的,可是如果是常規(guī)的兩個方法,他們的方法簽名(方法的名稱和參數(shù)類型)是一樣的,也就是說虛擬機根本不能分別這兩個方法。如果是我們自己編寫Java代碼,這樣的代碼是無法通過編譯器的檢查的,但是虛擬機卻是允許這樣做的,因為虛擬機通過參數(shù)類型和返回類型來確定一個方法,所以編譯器為了實現(xiàn)泛型的多態(tài)允許自己做這個看起來“不合法”的事情,然后交給虛擬器去區(qū)別。
不能用類型參數(shù)替換基本類型。就比如,沒有ArrayList<double>
,只有ArrayList<Double>
。因為當類型擦除后,ArrayList
的原始類型變?yōu)?code>Object,但是Object
類型不能存儲double
值,只能引用Double
的值。因為基本類型的父類不是Ojbect,兩者無關系。
ArrayList<String> arrayList = new ArrayList<String>();
因為類型擦除之后,ArrayList<String>
只剩下原始類型Object,泛型信息String
不存在了。
那么,運行時進行類型查詢的時候使用下面的方法是錯誤的
if( arrayList instanceof ArrayList<String>)
泛型類中的靜態(tài)方法和靜態(tài)變量不可以使用泛型類所聲明的泛型類型參數(shù)
舉例說明:
public class Test2<T> { public static T one; //編譯錯誤 public static T show(T one){ //編譯錯誤 return null; } }
因為泛型類中的泛型參數(shù)的實例化是在定義對象的時候指定的,而靜態(tài)變量和靜態(tài)方法不需要使用對象來調用。對象都沒有創(chuàng)建,如何確定這個泛型參數(shù)是何種類型,所以當然是錯誤的。
但是要注意區(qū)分下面的一種情況:
public class Test2<T> { public static <T >T show(T one){ //這是正確的 return null; } }
因為這是一個泛型方法,在泛型方法中使用的T是自己在方法中定義的 T,而不是泛型類中的T。
上述就是小編為大家分享的java泛型類型擦除的示例分析了,如果剛好有類似的疑惑,不妨參照上述分析進行理解。如果想知道更多相關知識,歡迎關注億速云行業(yè)資訊頻道。
免責聲明:本站發(fā)布的內容(圖片、視頻和文字)以原創(chuàng)、轉載和分享為主,文章觀點不代表本網(wǎng)站立場,如果涉及侵權請聯(lián)系站長郵箱:is@yisu.com進行舉報,并提供相關證據(jù),一經(jīng)查實,將立刻刪除涉嫌侵權內容。