[運行時獲取模板類類型] Java 反射機制 + 類型擦除機制

給定一個帶模板參數的類

class A<T> 

{

}

如何在運行時獲取 T的類型?

在C#中,這個很簡單,CLR的反射機制是解釋器支持的,大概代碼爲:

namespace TestReflect
{
    class Program<T>
    {

        public Type getTClass()
        {
            Type type= this.GetType();
 
            Type[] tts = type.GetGenericArguments();
            return tts[0];
        }
    }
}
可是在Java中,答案是否定的,java中沒有直接方法來獲取T.Class. (至少JDK 1.7以前不可能)

其根本原因在於: java語言是支持反射的,但是JVM不支持,因爲JVM具有“Type erasure”的特性,也就是“類型擦除”。

但是Java無絕人之路,我們依舊可以通過其他方法來達到我們的需求。

方法1. 通過構造函數傳入實際類別. 【有人說這個方法不帥氣,但是不可否認,這種方法沒有用到反射,但是最爲高效,反射本身就不是高效的。】

public class A<T>
{
    private final Class<T> clazz;

    public A<T>(Class<T> clazz)
    {
        this.clazz = clazz;
    }

    // Use clazz in here
}

方法2. 通過繼承這個類的時候傳入模板T對應的實際類:(定義成抽象類也可以)

class A<T>{

    public Class getTClass(int index) {
	 
	  Type genType = getClass().getGenericSuperclass();

          if (!(genType instanceof ParameterizedType)) 
          {
             return Object.class;
          }
  
 <span style="white-space:pre">	</span>  Type[] params = ((ParameterizedType) genType).getActualTypeArguments();

<span style="white-space:pre">	</span>  if (index >= params.length || index < 0) {
    <span style="white-space:pre">	</span>  <span style="white-space:pre">	</span>throw new RuntimeException("Index out of bounds");
 <span style="white-space:pre">	</span>  }
 
 <span style="white-space:pre">	</span>  if (!(params[index] instanceof Class)) {
   <span style="white-space:pre">		</span> return Object.class;
  <span style="white-space:pre">	</span>  }
   <span style="white-space:pre">	</span> return (Class) params[index];
    }
}

繼承類:

public class B extends A<String> {
	
	public static void main(String[] args) {
		  B bb =new B();
     	   	  bb.getTClass();//即答案
	 }
}


我相信很多人用過這樣類似的代碼,但是並不是每個人都真正瞭解了,爲什麼可以這樣做。

光是看代碼:

我們可以

getGenericSuperclass();

卻沒有:

getGenericclass();

爲什麼呢?

stackoverflow上有個老外說:java 裏如果 一個類繼承了另外一個帶模板參數的類,那麼這個模板參數不會被“類型擦除”。而單一的一個類,其泛型參數會被擦除。

首先說明這種假設是錯誤的。我相信JCP一定會不會莫名其妙的有這種無厘頭規定。如果這種說法成立,就等於:

public class C<T> {

}
public class C1<T> extends C<T>{

	public C1()
	{
	}

	 public Class getTClass(int index) {
		 
		  Type genType = getClass().getGenericSuperclass();
		  
		  if (!(genType instanceof ParameterizedType)) 
		  {
		      return Object.class;
		  }
		  
		  Type[] params = ((ParameterizedType) genType).getActualTypeArguments();
		  
		  if (index >= params.length || index < 0) {
		     throw new RuntimeException("Index outof bounds");
		  }
		  
		  if (!(params[index] instanceof Class)) {
		     return Object.class;
		  }
	      return (Class) params[index];
	 }
	
}
直接調用C1來創建實體後:

public static void main(String[]args)
	{
		C1<D> a= new C1<D>();
		System.out.println(a.getTClass(0));
	}

能輸出:D

但是實際情況是,沒有輸出D,輸出的是 Object.

說明這跟是不是父類子類沒關係。

那麼究竟是什麼原因,導致了,一個泛型類(含有模板參數的類),沒有像C#中 GetGenericArguments()類似的getGenericClass()函數呢??

再來溫習下java 中 “類型擦除” 的範疇。

我用自己的語言定義一下(未必精確,但求理同):

Java中所有除了【類聲明區域】(區域劃分如下)之外的代碼中,所有的泛型參數都會在編譯時被擦除。

public class/interface [類聲明區域] {
	
	[類定義區域]
			
}

如下的2個類:

public class E<T>{

	public static void main(String []args)
	{
		List<String> a = new ArrayList<String>();
		System.out.println("Done");
	}
}
class SubE extends E<String>{
	
}

在編譯後的class文件代碼爲:(By Java De-compiler)

public class E<T>
{
  public static void main(String[] args)
  {
    List a = new ArrayList(); //區別在這一行
    System.out.println("Done");
  }
}
class SubE extends E<String>
{
}

可以看到,【類聲明區域】和java文件中的一模一樣。而【類定義區域】中所有的泛型參數都被去掉了。

那麼爲啥這樣呢?一個類,在編程中宿命的只有兩大類:要麼被繼承,要麼自己創建實例。直接用於創建實例時必在【類定義區域】,從而必定被擦除。只有被繼承時,子類的實例信息中會存在一個夫類的泛型信息。

爲何要有類型擦除?這涉及到Java語言的特性,JDK 從1.5(應該是)開始支持泛型,但是隻能說是Java語法支持泛型了,JVM並不支持泛型,不少人笑稱其爲 “假泛型”。

雖然會被擦除,但是《Effective Java》 2th Edtion 還是建議大家在編程中,明確限定原型類,這樣可以更好的約束代碼,在編譯期間提示。如果確實不在乎列表元素的類型是否一致,請使用 List<?>。


所以當我們使用 

List<String>的時候,編譯器看到的不是String,而是一個Object(java中所有類型都繼承於Object)。

一旦【類定義區域】中的泛型參數被擦除了。那麼使用這個模板類創建實例,運行時,JVM反射是無法獲取此模板具體的類型的。

因此

像C#中 GetGenericArguments()類似的getGenericClass()函數,在java中毫無意義。

這裏的毫無意義是指在上面所說的java和jvm的特性的基礎上。(若jvm支持真泛型,那麼這一切就有意義了)

爲什麼說他無意義,反證法:

假設這個函數存在,有意義,那麼下面代碼可以獲取T.Class

class A<T>{
  public Class getTclass() 
  {
	      return this.getGenericClasses()[0];//這裏只有一個模板參數
  }
  public static void main(String []args)
  {
	  A<Integer> a= new A<Integer>();
	  System.out.print(a.getTclass());
  }
}
這樣的一段代碼會被編譯成:

class A<T>{
  public Class getTclass() 
  {
	      return this.getGenericClasses()[0];//這裏只有一個模板參數
  }
  public static void main(String []args)
  {
	  A a= new A();
	  System.out.print(a.getTclass());
  }
}

這時候問題就來了,JVM執行的是.class文件,而不是.java文件,在JVM看來,這個class文件裏沒有說任何關於T的信息,現在你問我要T.class 我該拿什麼給你?

這樣一來,

getGenericClasses()

這個函數永遠都返回 java.lang.Object. 等於沒返回。

所以這個函數沒有必要存在了。

回頭想想,之所以

getGenericSuperclass();有效,其本質在於

在子類的java文件中的【類聲明區域】指定了T的真正類。

如:

public class B extends A<Integer>{

//...

}



這樣一個小懸案就明朗了。













發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章