泛型的擦除

一,擦除的理解

对于Java中泛型的擦除,可以直接使用反射,通过代码来测试。关于反射反射的理解。

通过反射我们可以直接过去一个类中所有的属性(无论权限)与该类的所有信息。

先来看一段代码:

这段代码的输出结果后来为true。从这段代码中就可以看出泛型的擦除,不论<>中是填写的什么类型,或者类对象,在编译期时无论Integer还是String都会被擦除变成Object类型,这就是擦除。(概念可以在《java编程思想》P372详情查看)

package cn.fyihan;

import java.util.ArrayList;

public class CleanUpAllgeneric {
	
	public static void main(String[] args) {
		Class c1 = new ArrayList<String>().getClass();
		Class c2 = new ArrayList<String>().getClass();
		if(c1==c2){
			System.out.println(true);
		}else{
			System.out.println(false);
		}
		
	}

}

关于擦除的麻烦之处:

首先因为擦除的缘故导致我们无法获取泛型内部参数类型的信息。也就包括无法进行instanceof比较,无法调用参数类型中的方法。来查看以下代码:

这里虽然可以通过编译器来获取到user()方法,因为编译器的编译检测方式是可以按照泛型中的类型参数检测的。但是在编译期,因为擦除仍然会报错NullpointException。

public class CleanUpAllgeneric {
	
	public void user(){
		System.out.println("user this method");
	}
	
}
public class Fruit<T> {
	
	T a;
	public Fruit(){
	}
	
	public Fruit(T t){
		this.a =t;
	}

	public T getA() {
		return a;
	}

	public void setA(T a) {
		this.a = a;
	}
	
}
public class Apple {
	
	public static void main(String[] args) {
		Fruit<CleanUpAllgeneric> f = new Fruit<CleanUpAllgeneric>();
		f.getA().user();
	}
}

如果在Fruit总不设置get与set方法,直接在Fruit中的方法中进行t.user()会出现报错,因为在运行的过程 中已经进行了擦除,所以也无法获得user()方法

public class CleanUpAllgeneric {
	
	public void user(){
		System.out.println("user this method");
	}
	
}
public class Fruit<T> {
	
	T a;
	public Fruit(){
	}
	
	public Fruit(T t){
		this.a =t;
	}

	public void getUserMethod(){
                a.user();     //error
        }
}

二,擦除的应对:

方式一

对于泛型擦除存在的意义所在,我仍旧无法理解为什么这样麻烦的泛型还是会被应用,无论是<<java编程思想>>还是别人的博客中关于泛型擦除存在意义的解释。虽然实现了用户上的泛化可以减少代码量,但是也是让代码的可读性下降。结构变得复杂,也许这也是逐渐走向高端的所需?。。

对于擦除的应对,前面的例子可以发现,编译器其实对泛型<>中的类型参数进行了检索,要求保持一致,但是无法解决编译期擦除后无法调用方法和instanceof等问题。

在java引入了泛型中继承的方式来应对擦除,来看下面的代码:

如同上面的例子一样,也会在编译器上获取到方法的调用。但是这次运行后,得到的结果没有报空指针异常,获取到了方法中的内容。可以认为这里的T extends Pet相当于给了一个边界,就如同对编译器而言一样,给予了这个边界自然就可以使用它的方法,就好像告诉了他们这些被调用的类都是Pet的子类,都实现了eat和run方法,直接调用输出就行了。

interface Pet {	
	void eat();
	void run();
}
class Dog implements Pet{
	@Override
	public void eat() {
		System.out.println("狗在吃");
	}

	@Override
	public void run() {
		System.out.println("狗在跑");
	}
}
class Cat implements Pet{
	@Override
	public void eat() {
		System.out.println("猫在吃");
	}

	@Override
	public void run() {
		System.out.println("猫在跑");
	}
}
public class UserThis {
	
     static <T extends Pet> void resMyPet(T onePet){
    	 onePet.eat();
    	 onePet.run();
     }
     
     static void main(String[] args) {
		Cat cat = new Cat();
		Dog dog = new Dog();
		UserThis.resMyPet(cat);
		UserThis.resMyPet(dog);
	}

}

方式二

利用RTTI的方法,通过反射创建泛型类作为参数类型。

如下代码:

通过反射创建泛型类,因为擦除的原因,在newInstan()方法只是实例化的Object类型,所以在后续的isNewInstance()直接引入一个T类来实现的实例化类。仔细看这段代码,利用了工厂的模式,去掉泛型仿佛把所有的类放入进去都能实例化,但是编辑器是识别泛型的边界的,它可以识别你实例化的类和<>中类的比较。因为继承的向上转型导致Building时可以全部实例化,但是house时却只能实例化House。

public class Building {
	
	public void submit(){
		System.out.println("building.....");
	}
}
public class House extends Building{
	
	public void sumBit(){
		System.out.println("house...");
	}
}
public class ClassTypeCature<T> {
	
	Class<T> clazz; //泛型类
	T t;
	public ClassTypeCature(){
	}
	
	public ClassTypeCature(Class<T> t){
		this.clazz = t;
	}
	
	public void newInstan() throws InstantiationException, IllegalAccessException{
		t = clazz.newInstance();
	}
	
	public boolean isNewInstance(T t){
		return clazz.isInstance(t);
		
	}
	
	public static void main(String[] args) throws InstantiationException, IllegalAccessException {
		ClassTypeCature<Building> cb = new ClassTypeCature<Building>(Building.class);
		cb.newInstan();
		System.out.println(cb.isNewInstance(new Building()));
		System.out.println(cb.isNewInstance(new House()));
		ClassTypeCature<House> hb = new ClassTypeCature<House>(House.class);
		hb.newInstan();
		System.out.println(hb.isNewInstance(new House()));
		//System.out.println(hb.isNewInstance(new Building()));    error
	}
}

方式三

通过泛型抽象类中的抽象方法。

createClass()方法是在子类中实现的,通过编译器的检查直接返回new的Class对象,同时通过泛型类来限制传入。

public abstract class UseingClass<T> {
	abstract T createClass();
}
public class X {
}
public class UseIngX extends UseingClass<X> {

	@Override
	X createClass() {
		System.out.println("ok...");
		return new X();
	}
	
	public static void main(String[] args) {
		UseIngX uix = new UseIngX();
		uix.createClass();
	}
}

三,协变

协变是关于基类与子类中复杂性交错的过程。先来看一段关于通配符的协变代码:

输出结果会报存储异常。但是编译器并不会对向上转型的数组中存入Orange对象进行判断错误。

public class Fruit {
}
public class Orange extends Fruit{
}
public class Apple extends Fruit{
}
public class ContainArray {
	
	public static void main(String[] args) {
		Fruit[] fts = new Apple[10];  //向上转型获得的数组
		fts[0] = new Apple();
		fts[1] = new Orange();
		System.out.println(fts);
	}

}

关于通配符的协变了解了之后,泛型中的协变。可以看到ArrayList并不能像数组那样协变。泛型中要求的边界?extends Fruit,会导致list的get方法中也会出现? extends Fruit。这就会导致无法知道具体加入的是哪种类型,所以只能加入Null数据。但是编译器不会报错,因为编译器会知道参数类型继承自Fruit,所以只要是存入指定基类的一个子类就行。

public class ContainArrayII {
	public static void main(String[] args) {
		List<? extends Fruit> list  = new ArrayList<Apple>();
		//list.add(new Apple());   error
		//list.add(new Orange());  error
		//list.add(new Fruit());   error
		list.add(null);
		Fruit f = list.get(0);
		System.out.println(list.get(0));
		System.out.println(f);
	}
}

四,逆变

逆变可以认为是超类型通配符。和extends相反,来看一段代码:

逆变的方法中支持add()方法中添加具体的类型。

public class ContainArrayIII {
	
	public void getListNR(List<? super Fruit> list){
		list.add(new Fruit());
		list.add(new Apple());
		list.add(new Orange());
	}
}

 

 

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值