Java 泛型擦除


泛型擦除概念

Java的泛型是偽泛型,這是因為Java在編譯期間,所有的泛型信息都會被擦掉,正確理解泛型概念的首要前提是理解類型擦除。Java的泛型基本上都是在編譯器這個層次上實現的,在生成的字節碼中是不包含泛型中的類型信息的,使用泛型的時候加上類型參數,在編譯器編譯的時候會去掉,這個過程成為類型擦除。
例如:List<String>List<Integer> 在編譯后都變成 List

證明泛型擦除

         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 泛型類型,只能存儲字符串。一個是ArrayList 泛型類型,只能存儲整形。最后,我們通過arrayList1對象和arrayList2對象的getClass方法獲取它們的類的信息,最后發現結果為true。說明泛型類型String和Integer都被擦除掉了,只剩下了 原始類型。

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

在程序中定義了一個ArrayList泛型類型實例化為Integer的對象,如果直接調用add方法,那么只能存儲整形的數據。不過當我們利用反射調用add方法的時候,卻可以存儲字符串。這說明了Integer泛型實例在編譯之后被擦除了,只保留了 原始類型。

引用檢查與編譯

說類型變量會在編譯的時候擦除掉,那為什么我們往ArrayList arrayList=new ArrayList ();所創建的數組列表arrayList中,不能使用add方法添加整形呢?

java編譯器是通過先檢查代碼中泛型的類型,然后再進行類型擦除,在進行編譯的。

         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

類型檢查就是針對引用的,會對這個引用調用的方法進行類型檢測,而無關它真正引用的對象。

泛型擦除與多態的沖突和解決方法

泛型重載變重寫?

public class Pair<T> {
	private T value;
	public T getValue() {
		return value;
	}
	public void setValue(T value) {
		this.value = value;
	}
}

public class DateInter extends Pair<Date> {
	@Override
	public void setValue(Date value) {
		super.setValue(value);
	}
	@Override
	public Date getValue() {
		return super.getValue();
	}
}

在這個子類中,我們設定父類的泛型類型為Pair ,在子類中,我們覆蓋了父類的兩個方法,我們的原意是這樣的:

將父類的泛型類型限定為Date,那么父類里面的兩個方法的參數都為Date類型

    public Date getValue() {
		return value;
	}
	public void setValue(Date value) {
		this.value = value;
	}

實際上,類型擦除后,父類的的泛型類型全部變為了原始類型Object,而子類的類型是Date,參數類型不一樣,這如果實在普通的繼承關系中,根本就不會是重寫,而是重載

本意是將父類中的泛型轉換為Date類型,子類重寫參數類型為Date的兩個方法

實際上,虛擬機並不能將泛型類型變為Date,只能將類型擦除掉,變為原始類型Object。這樣,我們的本意是進行重寫,實現多態。可是類型擦除后,只能變為了重載。這樣,類型擦除就和多態有了沖突。JVM知道你的本意嗎?知道!!!可是它能直接實現嗎,不能!!!

如果真的不能的話,那我們怎么去重寫我們想要的Date類型參數的方法

JVM采用了一個特殊的方法,來完成這項功能,那就是橋方法

JVM橋方法

使用bytecode viewer插件反編譯DateInter

public class com/qhong/basic/genericity/DateInter extends com/qhong/basic/genericity/Pair {

  // compiled from: DateInter.java

  // access flags 0x1
  public <init>()V
   L0
    LINENUMBER 13 L0
    ALOAD 0
    INVOKESPECIAL com/qhong/basic/genericity/Pair.<init> ()V
    RETURN
   L1
    LOCALVARIABLE this Lcom/qhong/basic/genericity/DateInter; L0 L1 0
    MAXSTACK = 1
    MAXLOCALS = 1

  // access flags 0x1
  public setValue(Ljava/util/Date;)V
   L0
    LINENUMBER 16 L0
    ALOAD 0
    ALOAD 1
    INVOKESPECIAL com/qhong/basic/genericity/Pair.setValue (Ljava/lang/Object;)V
   L1
    LINENUMBER 17 L1
    RETURN
   L2
    LOCALVARIABLE this Lcom/qhong/basic/genericity/DateInter; L0 L2 0
    LOCALVARIABLE value Ljava/util/Date; L0 L2 1
    MAXSTACK = 2
    MAXLOCALS = 2

  // access flags 0x1
  public getValue()Ljava/util/Date;
   L0
    LINENUMBER 20 L0
    ALOAD 0
    INVOKESPECIAL com/qhong/basic/genericity/Pair.getValue ()Ljava/lang/Object;
    CHECKCAST java/util/Date
    ARETURN
   L1
    LOCALVARIABLE this Lcom/qhong/basic/genericity/DateInter; L0 L1 0
    MAXSTACK = 1
    MAXLOCALS = 1

  // access flags 0x1041
  public synthetic bridge setValue(Ljava/lang/Object;)V
   L0
    LINENUMBER 13 L0
    ALOAD 0
    ALOAD 1
    CHECKCAST java/util/Date
    INVOKEVIRTUAL com/qhong/basic/genericity/DateInter.setValue (Ljava/util/Date;)V
    RETURN
   L1
    LOCALVARIABLE this Lcom/qhong/basic/genericity/DateInter; L0 L1 0
    MAXSTACK = 2
    MAXLOCALS = 2

  // access flags 0x1041
  public synthetic bridge getValue()Ljava/lang/Object;
   L0
    LINENUMBER 13 L0
    ALOAD 0
    INVOKEVIRTUAL com/qhong/basic/genericity/DateInter.getValue ()Ljava/util/Date;
    ARETURN
   L1
    LOCALVARIABLE this Lcom/qhong/basic/genericity/DateInter; L0 L1 0
    MAXSTACK = 1
    MAXLOCALS = 1
}

我們本意重寫setValue和getValue方法的子類,竟然有4個方法,其實不用驚奇,最后的兩個方法,就是編譯器自己生成的橋方法。可以看到橋方法的參數類型都是Object,也就是說,子類中真正覆蓋父類兩個方法的就是這兩個我們看不到的橋方法。而打在我們自己定義的setvalue和getValue方法上面的@Oveerride只不過是假象。而橋方法的內部實現,就只是去調用我們自己重寫的那兩個方法。

所以,虛擬機巧妙的使用了巧方法,來解決了類型擦除和多態的沖突。

虛擬機兼容方法簽名相同

子類中的橋方法 Object getValue()和Date getValue()是同時存在的,可是如果是常規的兩個方法,他們的方法簽名是一樣的,也就是說虛擬機根本不能分別這兩個方法。如果是我們自己編寫Java代碼,這樣的代碼是無法通過編譯器的檢查的,但是虛擬機卻是允許這樣做的,因為虛擬機通過參數類型和返回類型來確定一個方法,所以編譯器為了實現泛型的多態允許自己做這個看起來“不合法”的事情,然后交給虛擬器去區別

<T> VS <?>

不同點:

  • <T> 用於 泛型的定義,例如 class MyGeneric<T> {...}
  • <?> 用於 泛型的聲明,即泛型的使用,例如 MyGeneric<?> g = new MyGeneric<>();

相同點:

都可以指定上界和下界,例如:
class MyGeneric<T extends Collection> {...}
class MyGeneric<T super List> {...}

MyGeneric<? extends Collection> g = new MyGeneric<>();`
 `MyGeneric<? super List> g = new MyGeneric<>();

靜態方法,靜態類中的泛型

泛型類中的靜態方法和靜態變量不可以使用泛型類所聲明的泛型類型參數

public class Test2<T> {  
        public static T one;   //編譯錯誤  
        public static  T show(T one){ //編譯錯誤  
            return null;  
        }  
} 

因為泛型類中的泛型參數的實例化是在定義對象的時候指定的,而靜態變量和靜態方法不需要使用對象來調用。對象都沒有創建,如何確定這個泛型參數是何種類型,所以當然是錯誤的。

靜態方法自己定義的泛型

public class Test2<T> {    
        public static <T >T show(T one){//這是正確的  
            return null;  
        }  
} 

因為這是一個泛型方法,在泛型方法中使用的T是自己在方法中定義的T,而不是泛型類中的T。

參考:

Java 泛型,你了解類型擦除嗎?

java泛型(二)、泛型的內部原理:類型擦除以及類型擦除帶來的問題


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM