首页 > 代码库 > [运行时获取模板类类型] Java 反射机制 + 类型擦除机制

[运行时获取模板类类型] 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 [类声明区域] className{
	
	[类定义区域]
			
}

如下的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并不支持泛型,不少人笑称其为 “假泛型”。

其实我个人觉得这也许正是Java语言一切都是对象,并且要求高度多态,高度面向抽象编程,弱类型的特点所铺垫的。

比如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>{

//...

}



这样一个小悬案就明朗了。













[运行时获取模板类类型] Java 反射机制 + 类型擦除机制