JAVA的泛型——如何获取参数化类型

泛型是一种元编程模式,如果这种语言具有元编程能力,就有可能使用泛型,如果这种语言的元编程对象可以是自己,即自我编写自我,那么就具有反射能力,结合反射可以极大的发挥元编程的价值。不同的语言的泛型实现方式不同,一个鲜明的对比就是C#的泛型跟JAVA的泛型,前者是独立类型模式,而后者则是静态共享模式,这导致了在开发过程中处理方式有很大不同。

参数化类型,指的就是泛型的实际形式,即将类型以参数的方式进行传递,而另一方如果接收了这个类型,就可以将这些类型用于自己的代码实现,因为这些类型是参数所以是不确定,泛型所涉及的一切都是动态的,不存在静态的泛型类或者静态泛型方法。

获取父类类型

public class A <T,U>{
	
	public void getPType() {
		//获取父类类型
		Type type = this.getClass().getGenericSuperclass();
		System.out.println(type);
		
	}
	
	public static void main(String[] args) {
		new A<User,Demo>().getPType();
	}
	
}

上面的代码中给A设置了两个类型参数:T,U,在实例化的时候对应的传入了两个类型User,Demo,A输出父类为:

class java.lang.Object
public class B extends A<User,Demo>{
	//这里没有传入A的泛型参数
	public static void main(String[] args) {
		new B().getPType();
	}
}

创建了一个A的子类B,B输出的父类为:

com.test.learn.A<com.entity.User, com.entity.Demo>

获取类型参数

在上述通过getGenericSuperclass得到的父类类型是一个复合元组Type,让我们来看看它包含了什么,我们分别在父类中增加并在子类中重写一个方法:

public class A <T,U>{
	
	public void getPType() {
		Type type = this.getClass().getGenericSuperclass();
		Type[] types = ((ParameterizedType)type).getActualTypeArguments();
		for(Type t:types){
			System.out.println(t);
		}
	}
	
	
	public static void main(String[] args) {
		new A<User,Demo>().getPType();
	}
	
}
public class B extends A<User,Demo>{
		
	public void getPType() {
		Type type = this.getClass().getGenericSuperclass();
		Type[] types = ((ParameterizedType)type).getActualTypeArguments();
		for(Type t:types){
			System.out.println(t);
		}
	}
	
	public static void main(String[] args) {
		new B().getPType();
	}
}

父类的执行结果:

Exception in thread "main" java.lang.ClassCastException: java.lang.Class cannot be cast to java.lang.reflect.ParameterizedType
	at com.test.learn.A.getPType(A.java:13)
	at com.test.learn.A.main(A.java:21)

子类的执行结果:

class com.entity.User
class com.entity.Demo

原因见API:

如果超类是参数化类型,则返回的 Type 对象必须准确反映源代码中所使用的实际类型参数。如果以前未曾创建表示超类的参数化类型,则创建这个类型。有关参数化类型创建过程的语义,请参阅 ParameterizedType 声明。如果此 Class 表示 Object 类、接口、基本类型或 void,则返回 null。如果此对象表示一个数组类,则返回表示 Object 类的 Class 对象。 

也就是说,如果父类没有参数化类型标识,则返回父类类型对象,否则返回标识的类型Class实例。

参数化类型的实际传递

通过上面的输出可以发现,获取一个类接收参数化类型,其得到类型是来自父类的,如上面的A得到的参数化类型是超类Object而不是我们传入的类型,而子类B得到的参数化类型是父类A。

例如上述A得到的不是我们通过构造方法new创建它时传入的类型,而是父类。

new A<User,Demo>().getPType();//无法得到此时的类型参数

子类泛型的类型擦除

原因在于JAVA是共享泛型模式,A的子类都共用A的模板,子类泛型标识在编译时就会被擦除,不会写入到实际子类类型中,自然就无法得到自身标识的类型参数。

要得到泛型参数,所以只能从上限类型中得到。因为所有的泛型类在字节码阶段参数类型会被擦除至上限类型,如B<T,C> extends A<User,Demo>,那么在编译后T,C会被擦除,从而只能得到父类的User,Demo。你也可以理解为:父类的泛型是通过子类来确定并传达的。

public class B<T,C> extends A<User,Demo>{
		
	public void getPType() {
		Type type = this.getClass().getGenericSuperclass();
		Type[] types = ((ParameterizedType)type).getActualTypeArguments();
		for(Type t:types){
			System.out.println(t);
		}
	}
	
	public static void main(String[] args) {
		new B<Blog,Fans>().getPType();
	}
}

输出:

class com.entity.User
class com.entity.Demo

如果你希望动态的传入类型参数,例如你希望将上述的User,Demo改为你创建时传入的Blog,Fans类型,即在运行时将Blog,Fans这两个类型通过子类传给父类,那么java是不支持这种操作的。即使在子类编译时没有给父类确定的泛型参数,而是运行时具体化中传入,也就无法改变父类的泛型参数,这一切都是因为编译期确定机制

public class UserA<T> extends User<T>{

	public void getPType() {
		Type type = this.getClass().getGenericSuperclass();
		Type[] types = ((ParameterizedType)type).getActualTypeArguments();
		for(Type t:types){
			System.out.println(t);//得到的仍然是T的字面符号(T),而不是String
		}
	}
	
	public static void main(String[] args) {
		new UserA<String>().getPType();
	}
	
	
}

编译期确定原则是静态代码中的描述必需能提供一个确定的参数值,否则就存在不确定性,这是不允许的,结果就是语法错误、报错。但是在上述代码这种情况下,已经可以确定泛型参数T的类型为String,为什么执行泛型结果为T,而不是String?这也太说不过去了吧,这一切只能归功于子类泛型的类型擦除了,在编译期,类的定义会被编译,然后编译方法,将方法和类绑定。父类编译的时候没有得到具体的泛型参数,参数就只能编译成字节码写死了,编译后,子类就不再有泛型参数,所以我们传入的String其实是对父类不起作用的。

为什么子类泛型的类型擦除

面向对象开发语言中泛型的实现机制分为两类,java是擦除类型的,而C#是独立副本的。这有什么区别呢:

1、擦除类型只为父类保留一个副本,独立类型根据泛型而产生一一对应的副本

2、由于上面第一点,编译后包的大小,擦除类型比独立类型要小得多

3、擦除类型的泛型在子类中是无法获取的,而独立类型是可以获取到实际类型

其实提到java的类型擦除,有很多异闻认为这是java开发团队偷懒留下的坑,而真正的原因并非如此。不管是独立类型也好擦除也好,都由优缺点,而java在这方面做了取舍,其中最看重的一点就是兼容性,擦除类型能够让使用者无须重新编译零改动,已经非常难得,反之,一来jvm要大改,二来用户未必能接受,群体会分裂,对生态圈造成毁灭性打击。但是C#本来用户群体小得多。

尽管子类泛型的类型擦除,JAVA的类型参数传递通常用于静态元编程,例如子类定义的泛型参数可以用于方法级别的类型参数传递,也是挺实用的:

public class B<T,V> extends A<User,Demo>{
		
	public T getTarget(V value){
         return value.process(T);
        
     }


public static void main(){

    new B<Blog,Fans>().getTarget(new Fans());//传入的泛型参数被用于方法参数具体化

}
	
}

类似的有常见的new HashMap<String,String>();这就泛型参数的具体化

方法自身也可以利用泛型参数具体化:

public <V> v method(V nv);实际中则可以具体化为:User user = method(User u)。

你是否发现,java所有的泛型具体化都是依赖于编译时能否确定,如果编译时不能确定泛型参数(即动态),那么是无法编译通过的。所以不能像C#那样做动态元编程,这就是他们的差别之一。因为C#在设计的时候充分考虑到这一点,所以不存在上述的问题。

注意事项

类级别的类型参数传递,通过extends 父类<>来完成,而非下面的写法,容易混淆的是进行这样一种写法:

这种写法(写在class的声明中)并不能将类型参数传入,这里的标识符用作参数的占位符,它表示具有可传入的类型参数个数和位置,而不是类型的传入。

public class A <User,Demo>

这种写法是不正确的,当定义一个类的参数化类型的时候,当中的标识符代表的是参数名,而不是具体类,这跟方法的参数定义同理,如果使用具体类名,会有警告但不会阻止你书写,因为当你在调用这个类的实例的时候,有可能出现参数名跟具体类重名,如:new A<User,Demo>(),导致编译不通过。而B extends A<User,Demo>是传入类型,不是参数定义,因此可以给定具体的类名。

另外,通过TypeVariable<?>[] types = this.getClass().getTypeParameters();可以得到参数名称。

如果父类没有传入确定的类型,则子类的类型也必需声明相同的参数名,因为java不允许一个编译时没有确定的泛型参数:

public class UserA extends User<T>{//报错T cannot be resolved to a type

正确的写法:

public class B<T,U> extends A<T,U>

而对于已经确定的类型,子类无需再声明,但不限于父类不确定的类型和顺序:

public class B<U> extends A<User,U>
public class B<U,K> extends A<User,U>

不管怎样,java泛型参数仅限于静态元编程,运行时只能得到上限类型的泛型参数,需要获取类型参数的时候,通常只需要记住一点,那就是:将得到父类的类型参数,以及个数,都取决于父类。

  • 3
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值