[运行时获取模板类类型] 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>{

//...

}

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

时间: 2024-12-28 02:19:33

[运行时获取模板类类型] Java 反射机制 + 类型擦除机制的相关文章

iOS运行时编程(Runtime Programming)和Java的反射机制对比

运行时进行编程,类似Java的反射.运行时编程和Java反射的对比如下: 1.相同点 都可以实现的功能:获取类信息.属性设置获取.类的动态加载(NSClassFromString(@“className”)).方法的动态调用 下面是iOS中涉及到的相关使用方法 类的动态加载:NSClassFromString(@“className”),方法的动态调用:NSSelectorFormString(@”doSonethingMethod:”) 常见的方法: isKindOfClass: isMemb

iOS开发——运行时OC篇&amp;使用运行时获取系统的属性:使用自己的手势修改系统自带的手势

使用运行时获取系统的属性:使用自己的手势修改系统自带的手势 有的时候我需要实现一个功能,但是没有想到很好的方法或者想到了方法只是那个方法实现起来太麻烦,一或者确实为了装逼,我们就会想到iOS开发中最牛逼的技术运行时(Runtime) 关于运行时这里就不多说了,请查看笔者之前搬过来了精选文章,这里主要是怎么在实际开发中使用运行时实现我们想要的功能. 比如,在iOS开发中有这样一个问题,关于导航栏中我们点击一个按钮会跳到对应的子控制器,但是这里问题就来了,我们在对应的字控制器中可以点击对应的back

SWF运行时判断两个DisplayObject是否同个类型,属于flash professional库中的同一个元件

一般我们判断两个实例对象是否同样的类型,可以用typeof得到对象类型,然后用==号比较. typeof适用于原生类型. 而对于自定义类型,虽然typeof得到的都是Object,但还有更强的招数:getQualifiedClassName 利用这个原生函数可以获取到两个实例的真实类型. 然而,对于Flash professional制作出来的swf,运行时要知道其中两个MovieClip是否来自库里边的同一个元件,上述方法都无能为力了. 本文就是探讨这个问题. 1.首先,想到的是,如果两个实例

Android PermissionUtils:运行时权限工具类及申请权限的正确姿势

Android PermissionUtils:运行时权限工具类及申请权限的正确姿势 ifadai 关注 2017.06.16 16:22* 字数 318 阅读 3637评论 1喜欢 6 PermissionUtil 经常写Android运行时权限申请代码,每次都是复制过来之后,改一下权限字符串就用,把代码搞得乱糟糟的,于是便有了封装工具类的想法,话不多说,先看怎么用: 工具类及Demo:github 简洁版申请权限 申请一个权限: PermissionUtils.checkAndRequest

能否向编译后得到的类中增加实例变量?能否向运行时创建的类中添加实例变量?为什么

不能向编译后得到的类中增加实例变量!能向运行时创建的类中添加实例变量! 因为编译后的类已经注册在runtime中,类结构体中的objc_ivar_list 实例变量的链表和instance_size实例变量的内存大小已经确定,同时runtime 会调用class_setIvarLayout 或 class_setWeakIvarLayout来处理strong weak引用,所以不能向存在的类中添加实例变量. 运行时创建的类是可以添加实例变量,调用 class_addIvar 函数,但是得在调用 

Swift 3.0 运行时获取类属性

在OC中,没有绝对的私有,用运行时可以取出. 在Swift中,有绝对的私有,用private修饰的,是无法取出的. 以下是Swift获取类属性的方法: ////  Person.swift// import UIKit class Person: NSObject {    var name: String?    var age:Int = 0   private var title: String?        class func propertyList( ) {           

Objective-C Runtime 运行时之一:类与对象

Objective-C语言是一门动态语言,它将很多静态语言在编译和链接时期做的事放到了运行时来处理.这种动态语言的优势在于:我们写代码时能够更具灵活性,如我们可以把消息转发给我们想要的对象,或者随意交换一个方法的实现等. 这种特性意味着Objective-C不仅需要一个编译器,还需要一个运行时系统来执行编译的代码.对于Objective-C来说,这个运行时系统就像一个操作系统一样:它让所有的工作可以正常的运行.这个运行时系统即Objc Runtime.Objc Runtime其实是一个Runti

[ObjectC]Runtime 运行时之一:类与对象

Objective-C语言是一门动态语言,它将很多静态语言在编译和链接时期做的事放到了运行时来处理.这种动态语言的优势在于:我们写代码时更具灵活性,如我们可以把消息转发给我们想要的对象,或者随意交换一个方法的实现等. 这种特性意味着Objective-C不仅需要一个编译器,还需要一个运行时系统来执行编译的代码.对于Objective-C来说,这个运行时系统就像一个操作系统一样:它让所有的工作可以正常的运行.这个运行时系统即Objc Runtime.Objc Runtime其实是一个Runtime

Runtime 运行时之一:类与对象

Objective-C语言是一门动态语言,它将很多静态语言在编译和链接时期做的事放到了运行时来处理.这种动态语言的优势在于:我们写代码时能够更具灵活性,如我们可以把消息转发给我们想要的对象,或者随意交换一个方法的实现等. 这种特性意味着Objective-C不仅需要一个编译器,还需要一个运行时系统来执行编译的代码.对于Objective-C来说,这个运行时系统就像一个操作系统一样:它让所有的工作可以正常的运行.这个运行时系统即Objc Runtime.Objc Runtime其实是一个Runti