微信公眾號【黃小斜】大廠程序員,互聯網行業新知,終身學習踐行者。關注后回復「Java」、「Python」、「C++」、「大數據」、「機器學習」、「算法」、「AI」、「Android」、「前端」、「iOS」、「考研」、「BAT」、「校招」、「筆試」、「面試」、「面經」、「計算機基礎」、「LeetCode」 等關鍵字可以獲取對應的免費學習資料。
參考:java核心技術
一、Java泛型的實現方法:類型擦除
前面已經說了,Java的泛型是偽泛型。為什么說Java的泛型是偽泛型呢?因為,在編譯期間,所有的泛型信息都會被擦除掉。正確理解泛型概念的首要前提是理解類型擦出(type erasure)。
Java中的泛型基本上都是在編譯器這個層次來實現的。在生成的Java字節碼中是不包含泛型中的類型信息的。使用泛型的時候加上的類型參數,會在編譯器在編譯的時候去掉。這個過程就稱為類型擦除。
如在代碼中定義的List<object>和List<String>等類型,在編譯后都會編程List。JVM看到的只是List,而由泛型附加的類型信息對JVM來說是不可見的。Java編譯器會在編譯時盡可能的發現可能出錯的地方,但是仍然無法避免在運行時刻出現類型轉換異常的情況。類型擦除也是Java的泛型實現方法與C++模版機制實現方式之間的重要區別。
可以通過兩個簡單的例子,來證明java泛型的類型擦除。例1、
- 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());
- }
- }
例2、
- public class Test4 {
- public static void main(String[] args) throws IllegalArgumentException, SecurityException, IllegalAccessException, InvocationTargetException, NoSuchMethodException {
- ArrayList<Integer> arrayList3=new ArrayList<Integer>();
- arrayList3.add(1);//這樣調用add方法只能存儲整形,因為泛型類型的實例為Integer
- arrayList3.getClass().getMethod("add", Object.class).invoke(arrayList3, "asd");
- for (int i=0;i<arrayList3.size();i++) {
- System.out.println(arrayList3.get(i));
- }
- }
二、類型擦除后保留的原始類型
在上面,兩次提到了原始類型,什么是原始類型?原始類型(raw type)就是擦除去了泛型信息,最后在字節碼中的類型變量的真正類型。無論何時定義一個泛型類型,相應的原始類型都會被自動地提供。類型變量被擦除(crased),並使用其限定類型(無限定的變量用Object)替換。
例3:
- class Pair<T> {
- private T value;
- public T getValue() {
- return value;
- }
- public void setValue(T value) {
- this.value = value;
- }
- }
- class Pair {
- private Object value;
- public Object getValue() {
- return value;
- }
- public void setValue(Object value) {
- this.value = value;
- }
- }
從上面的那個例2中,我們也可以明白ArrayList<Integer>被擦除類型后,原始類型也變成了Object,所以通過反射我們就可以存儲字符串了。
如果類型變量有限定,那么原始類型就用第一個邊界的類型變量來替換。
比如Pair這樣聲明
例4:
- public class Pair<T extends Comparable& Serializable> {
注意:
如果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); //這兩個參數都是Integer,所以T為Integer類型
- Number f=Test2.add(1, 1.2);//這兩個參數一個是Integer,以風格是Float,所以取同一父類的最小級,為Number
- Object o=Test2.add(1, "asd");//這兩個參數一個是Integer,以風格是Float,所以取同一父類的最小級,為Object
- /**指定泛型的時候*/
- int a=Test2.<Integer>add(1, 2);//指定了Integer,所以只能為Integer類型或者其子類
- int b=Test2.<Integer>add(1, 2.2);//編譯錯誤,指定了Integer,不能為Float
- Number c=Test2.<Number>add(1, 2.2); //指定為Number,所以可以為Integer和Float
- }
- //這是一個簡單的泛型方法
- 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());
- }
三、類型擦除引起的問題及解決方法
因為種種原因,Java不能實現真正的泛型,只能使用類型擦除來實現偽泛型,這樣雖然不會有類型膨脹的問題,但是也引起了許多新的問題。所以,Sun對這些問題作出了許多限制,避免我們犯各種錯誤。
1、先檢查,在編譯,以及檢查編譯的對象和引用傳遞的問題
既然說類型變量會在編譯的時候擦除掉,那為什么我們往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);//編譯錯誤
- }
那么,這么類型檢查是針對誰的呢?我們先看看參數化類型與原始類型的兼容
以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);//返回類型就是String
- ArrayList arrayList2=new ArrayList<String>();
- arrayList2.add("1");//編譯通過
- arrayList2.add(1);//編譯通過
- Object object=arrayList2.get(0);//返回類型就是Object
- new ArrayList<String>().add("11");//編譯通過
- new ArrayList<String>().add(22);//編譯錯誤
- String string=new ArrayList<String>().get(0);//返回類型就是String
- }
- }
通過上面的例子,我們可以明白,類型檢查就是針對引用的,誰是一個引用,用這個引用調用泛型方法,就會對這個引用調用的方法進行類型檢測,而無關它真正引用的對象。
從這里,我們可以再討論下 泛型中參數化類型為什么不考慮繼承關系
在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;//編譯錯誤
在看第二種情況,將第二種情況拓展成下面的形式:
- ArrayList<String> arrayList1=new ArrayList<String>();
- arrayList1.add(new String());
- arrayList1.add(new String());
- ArrayList<Object> arrayList2=arrayList1;//編譯錯誤
所以,要格外注意,泛型中的引用傳遞的問題。
2、自動類型轉換
因為類型擦除的問題,所以所有的泛型類型變量最后都會被替換為原始類型。這樣就引起了一個問題,既然都被替換為原始類型,那么為什么我們在獲取的時候,不需要進行強制類型轉換呢?看下ArrayList和get方法:
- public E get(int index) {
- RangeCheck(index);
- return (E) elementData[index];
- }
看以看到,在return之前,會根據泛型變量進行強轉。
寫了個簡單的測試代碼:
- public class Test {
- public static void main(String[] args) {
- ArrayList<Date> list=new ArrayList<Date>();
- list.add(new Date());
- Date myDate=list.get(0);
- }
然后反編了下字節碼,如下
- public static void main(java.lang.String[]);
- Code:
- 0: new #16 // class java/util/ArrayList
- 3: dup
- 4: invokespecial #18 // Method java/util/ArrayList."<init
- :()V
- 7: astore_1
- 8: aload_1
- 9: new #19 // class java/util/Date
- 12: dup
- 13: invokespecial #21 // Method java/util/Date."<init>":()
- 16: invokevirtual #22 // Method java/util/ArrayList.add:(L
- va/lang/Object;)Z
- 19: pop
- 20: aload_1
- 21: iconst_0
- 22: invokevirtual #26 // Method java/util/ArrayList.get:(I
- java/lang/Object;
- 25: checkcast #19 // class java/util/Date
- 28: astore_2
- 29: return
看第22 ,它調用的是ArrayList.get()方法,方法返回值是Object,說明類型擦除了。然后第25,它做了一個checkcast操作,即檢查類型#19, 在在上面找#19引用的類型,他是
9: new #19 // class java/util/Date
是一個Date類型,即做Date類型的強轉。
所以不是在get方法里強轉的,是在你調用的地方強轉的。
附關於checkcast的解釋:
checkcast checks that the top item on the operand stack (a reference to an object or array) can be cast to a given type. For example, if you write in Java:
return ((String)obj);
then the Java compiler will generate something like:
aload_1 ; push -obj- onto the stack
checkcast java/lang/String ; check its a String
areturn ; return it
checkcast is actually a shortand for writing Java code like:
if (! (obj == null || obj instanceof <class>)) {
throw new ClassCastException();
}
// if this point is reached, then object is either null, or an instance of
// <class> or one of its superclasses.
3、類型擦除與多態的沖突和解決方法
現在有這樣一個泛型類:
- 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();
- }
- }
將父類的泛型類型限定為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();
- }
我們在一個main方法測試一下:
- public static void main(String[] args) throws ClassNotFoundException {
- DateInter dateInter=new DateInter();
- dateInter.setValue(new Date());
- dateInter.setValue(new Object());//編譯錯誤
- }
為什么會這樣呢?
原因是這樣的,我們傳入父類的泛型類型是Date,Pair<Date>,我們的本意是將泛型類變為如下:
- class Pair {
- private Date value;
- public Date getValue() {
- return value;
- }
- public void setValue(Date value) {
- this.value = value;
- }
- }
可是由於種種原因,虛擬機並不能將泛型類型變為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 // 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這兩個橋方法的意義又有不同。
setValue方法是為了解決類型擦除與多態之間的沖突。
而getValue卻有普遍的意義,怎么說呢,如果這是一個普通的繼承關系:
那么父類的setValue方法如下:
- public ObjectgetValue() {
- return super.getValue();
- }
- public Date getValue() {
- return super.getValue();
- }
關於協變:。。。。。。
並且,還有一點也許會有疑問,子類中的巧方法 Object getValue()和Date getValue()是同 時存在的,可是如果是常規的兩個方法,他們的方法簽名是一樣的,也就是說虛擬機根本不能分別這兩個方法。如果是我們自己編寫Java代碼,這樣的代碼是無法通過編譯器的檢查的,但是虛擬機卻是允許這樣做的,因為虛擬機通過參數類型和返回類型來確定一個方法,所以編譯器為了實現泛型的多態允許自己做這個看起來“不合法”的事情,然后交給虛擬器去區別。
4、泛型類型變量不能是基本數據類型
不能用類型參數替換基本類型。就比如,沒有ArrayList<double>,只有ArrayList<Double>。因為當類型擦除后,ArrayList的原始類型變為Object,但是Object類型不能存儲double值,只能引用Double的值。
5、運行時類型查詢
舉個例子:
- ArrayList<String> arrayList=new ArrayList<String>();
因為類型擦除之后,ArrayList<String>只剩下原始類型,泛型信息String不存在了。
那么,運行時進行類型查詢的時候使用下面的方法是錯誤的
- if( arrayList instanceof ArrayList<?>)
? 是通配符的形式 ,將在后面一篇中介紹。
6、異常中使用泛型的問題
1、不能拋出也不能捕獲泛型類的對象。事實上,泛型類擴展Throwable都不合法。例如:下面的定義將不會通過編譯:
- public class Problem<T> extends Exception{......}
- try{
- }catch(Problem<Integer> e1){
- 。。
- }catch(Problem<Number> e2){
- ...
- }
- try{
- }catch(Problem<Object> e1){
- 。。
- }catch(Problem<Object> e2){
- ...
- try{
- }catch(Exception e1){
- 。。
- }catch(Exception e2){//編譯錯誤
- ...
2、不能再catch子句中使用泛型變量
- public static <T extends Throwable> void doWork(Class<T> t){
- try{
- ...
- }catch(T e){ //編譯錯誤
- ...
- }
- }
- public static <T extends Throwable> void doWork(Class<T> t){
- try{
- ...
- }catch(T e){ //編譯錯誤
- ...
- }catch(IndexOutOfBounds e){
- }
- }
但是在異常聲明中可以使用類型變量。下面方法是合法的。
- public static<T extends Throwable> void doWork(T t) throws T{
- try{
- ...
- }catch(Throwable realCause){
- t.initCause(realCause);
- throw t;
- }
7、數組(這個不屬於類型擦除引起的問題)
不能聲明參數化類型的數組。如:
- Pair<String>[] table = newPair<String>(10); //ERROR
- Object[] objarray =table;
- objarray ="Hello"; //ERROR
- objarray =new Pair<Employee>();
8、泛型類型的實例化
不能實例化泛型類型。如,
- first = new T(); //ERROR
是錯誤的,類型擦除會使這個操作做成new Object()。
不能建立一個泛型數組。
- public<T> T[] minMax(T[] a){
- T[] mm = new T[2]; //ERROR
- ...
- }
類似的,擦除會使這個方法總是構靠一個Object[2]數組。但是,可以用反射構造泛型對象和數組。
利用反射,調用Array.newInstance:
- publicstatic <T extends Comparable> T[]minmax(T[] a)
- {
- T[] mm == (T[])Array.newInstance(a.getClass().getComponentType(),2);
- ...
- // 以替換掉以下代碼
- // Obeject[] mm = new Object[2];
- // return (T[]) mm;
- }
9、類型擦除后的沖突
1、
當泛型類型被擦除后,創建條件不能產生沖突。如果在Pair類中添加下面的equals方法:
- class Pair<T> {
- public boolean equals(T value) {
- return null;
- }
- }
booleanequals(String); //在Pair<T>中定義
boolean equals(Object); //從object中繼承
但是,這只是一種錯覺。實際上,擦除后方法
boolean equals(T)
變成了方法 boolean equals(Object)
這與Object.equals方法是沖突的!當然,補救的辦法是重新命名引發錯誤的方法。
2、
泛型規范說明提及另一個原則“要支持擦除的轉換,需要強行制一個類或者類型變量不能同時成為兩個接口的子類,而這兩個子類是同一接品的不同參數化。”
下面的代碼是非法的:
- class Calendar implements Comparable<Calendar>{ ... }
- class GregorianCalendar extends Calendar implements Comparable<GregorianCalendar>{...} //ERROR
這一限制與類型擦除的關系並不很明確。非泛型版本:
- class Calendar implements Comparable{ ... }
- class GregorianCalendar extends Calendar implements Comparable{...} //ERROR
10、泛型在靜態方法和靜態類中的問題
泛型類中的靜態方法和靜態變量不可以使用泛型類所聲明的泛型類型參數
舉例說明:
但是要注意區分下面的一種情況:
因為這是一個泛型方法,在泛型方法中使用的T是自己在方法中定義的T,而不是泛型類中的T。