笔试题总结——泛型通配符与类型擦除

笔试题总结——泛型通配符与类型擦除

  关于泛型通配符的相关概念,网上随便一搜一大堆,这里简单的说一下概念即可。

   ? extends 类:取得泛型上限——可用在类或方法上,只能取得值,不能修改值
     用在类上:T extends 类:T必须为类或者类的子类(不能用?)
     用在方法上:? extends 类:只能接收类或者其子类的泛型类,只能取得类中属性值,不能修改值
   ? super 类:取得泛型下限——只能用于方法中,可以取得值,也可以修改值
     用在方法上:除了可以取值,也可以设置属性值
   ?:相当于是第一种情况的一个特例,实际上为? extends Object,但只能用于方法上,同样不能修改值。
     用在方法上:表示参数可以接收任意类型的泛型类,只能取得类中数据,不能修改数据。

  这些概念是比较容易理解的。这里特别说明一下:取得值表现在类上就是getXX()方法,表现在方法上就是返回值,而设置值表现在类上就是setXX()方法,表现在方法上就是调用方法时的实参。
  但是,有没有想过下面几个问题:

  为什么使用上限(上界)通配符后,只能取得值而不能设置值?什么类型的值才能被设置?
  为什么使用下限(下界)通配符后,既能取得值也能设置值?取得的值是什么类型的?什么类型的值才能被设置

  解决上面几个问题前,先来看看类型擦除:

  类型擦除:泛型信息只存在于代码编译阶段,在进⼊ JVM 之前,与泛型相关的信息会被擦除掉。(通俗地讲:泛型类只存在于敲代码期间,当程序编译后泛型类就和普通类一模一样了

  由于存在类型擦除的存在,我们可以得知:泛型的存在仅仅是为了编写阶段相对简单,实际上在进入JVM前就被类型擦除了。
  类型擦除的规则如下:

  若泛型类的类型参数若没有指定上限,会被擦除为Object类型。
  若指定了上限,则类型参数被替换为相应的类型上限。

  有了类型擦除的概念及规则,接下来一步步分析解答上面提出的几个问题。
  举例说明:
  先定义定义一个Eat类及其子类Fruit,以及Fruit的两个子类Apple和Banana。
	class Eat{
	    public void description(){
	        System.out.println("this is Eat");
    	}
	}
	
	class Fruit extends Eat{
	    public void description(){
	        System.out.println("this is Fruit");
	    }
	}
	
	class Apple extends Fruit{
	    public void description(){
	        System.out.println("this is Apple");
	    }
	}
	
	class Banana extends Fruit{
	    public void description(){
	        System.out.println("this is Banana");
	    }
	}
  再直面这些问题前,先看看当上下界泛型出现在集合中的情形。
  创建一个addMethod1()函数测试泛型上限
    public static void addMethod1(List<? extends Fruit> list){
		//	list.add(new Apple());
        Fruit fruit = list.get(0);
    }
  此时不能给list中添加值,由于类型参数已经指定了上界Fruit,只要是Fruit类或其子类都可以添加到List中,但编译器并不知道具体是哪个类的元素被添加进入,若允许添加在类型擦除后可能会导致类型错误。此时List的add()方法即为:
	void add(? extends Fruit value)
  而当类型擦除后,实际上add()方法就变为了:
	void add(Fruit value)
  这也就说明,此时只要是Fruit类及其子类的对象都可以加到该集合中,这明显与集合的定义矛盾。因此此时不允许对集合进行添加。
  举个栗子:若先添加10个Apple对象,则该List在泛型擦除后就是一个包含10个Apple对象的集合,但若再添加1个Banana对象,Banana对象也是Fruit的子类,按照逻辑也是能添加进来的,但此时,这个List集合中既有Apple对象又有Banana对象,严重违背了集合的定义,因此不允许进行值的添加。
  而从list中取得元素是没有问题的,由于编译器并不知道当前类到底是Fruit类还是其子类,因此取出的元素即为Fruit类。相当于此时List的get()方法变为了:
	//	类型擦除前
	? extends Fruit get();
  而当类型擦除后,实际上get()方法就变为了:
	// 类型擦除后(若指定了上限,则类型参数被替换为相应的类型上限)
	Fruit get();
  此时只要使用Fruit类的引用来接收get()的返回值即可。需要注意只能使用该类接收而不能使用子类接收(不满足向上转型)
  同样的创建一个addMethod2()函数测试泛型下限
	public static void addMethod2(List<? super Fruit> list){
	        list.add(new Apple());
			//	list.add(new Eat());
	        Object object = list.get(0);
	    }
  与泛型上限不同的是,此时可以给list中添加元素,但有一个要求,此时添加的对象必须是Fruit类及其子类的对象。同样的看一下此时List的add()方法:
	void add(? super Fruit value)
  在类型擦除后,实际上add()方法就变为了:
	void add(Object value)
  类型擦除完后,add()的形参变为了Object类型,但此时只能插入Fruit及其子类,虽然知道Fruit是基类,但并不知道是往上数多少级的基类。简单的讲,无论传入的这个类是Fruit类或其父类,若是Fruit类,Fruit类的集合中包含Fruit及其子类对象没有问题,若是Fruit类的父类,既然Fruit都是该类的子类,则Fruit的所有子类也是该类的子类,因此包含Fruit及其子类也没有问题。
  但若不是Fruit及其子类则有明显的问题,根本不知道这个类与传入的类是什么关系,可能会导致类型错误。
  说的还是不够清楚,举个栗子:如果现在这个集合最终的类为Eat类(Fruit类的父类),则此时传入10个Fruit对象,10个Apple对象,10个Banana对象都是没有问题的。因为这些都是Eat类的子类(父类集合中可以存放子类对象—多态)。而若此时传入一个非Fruit及其子类(即Fruit的一个父类,假设存在为Water),此时,Eat类和Water类之间的关系并不能确定,因此,不能直接将Water类的对象放在Eat类集合中,以免出现错误。
  而从list中取得元素时取出来的对象类型只能是Object,由于编译器并不知道当前类到底是Fruit类还是其父类,因此不能直接赋值给Fruit类。相当于此时List的get()方法变为了:
	//	类型擦除前
	? super Fruit get();
  而当类型擦除后,实际上get()方法就变为了:
	// 类型擦除后(若泛型类的类型参数若没有指定上限,会被擦除为Object类型)
	Object get();
  由此也就不难理解为何返回的类型是Object了。
  现在再看上面的问题就很容易理解了。只需要注意类型擦除前后的结果即可(依旧用上面Fruit的例子)。
  泛型上限时,实际上setXX()与getXX()变为了:
	//	类型擦除前
	void setXX(? extends Fruit fruit);
	? extends Fruit getXX();
  而当类型擦除后,实际上get()方法就变为了:
	//	类型擦除后
	void setXX(Fruit fruit);
	Fruit getXX();
  对于setXX(Fruit fruit)来说,形参为Fruit类,也就是说只要传入的对象是Fruit类及其子类都可以,而并不知道Fruit类及其子类与当前类的到底有没有关系,因此会产生类型错误。
  举个栗子:

	class Person<T>{
	    private T first;
	
	    public T getFirst() {
	        return first;
	    }
	
	    public void setFirst(T first) {
	        this.first = first;
	    }
	}
	
	public static void main(String[] args) {
	        Person<? extends Fruit> person = new Person<>();
	        //	不能设置值
	        //	person.setFirst(new Fruit());
	        person.getFirst();
	 }
  此时在Person内部解析的setFirst()只知道当前的类型是Fruit类型或其子类,但到底是哪个子类也不得而知,因此,若Person内部是Apple(Fruit的一个子类),而此时外部传了一个Fruit对象,此时就导致了严重的对象类型错误。(不符合多态)
  此时对于getFirst()来说,无论Person内部是Fruit的哪一个子类,但都会是Fruit的子类,因此外部使用Fruit引用接收Fruit或其子类对象是没有问题的。
  同样的,对于泛型下限来说,实际的setXX与getXX变为了:
	//	类型擦除前
	void set(? super Fruit fruit)
	? super Fruit get();
  而当类型擦除后,实际上get()方法就变为了:
	// 类型擦除后(若泛型类的类型参数若没有指定上限,会被擦除为Object类型)
	void set(Object fruit)
	Object get();
  对于setXX(Fruit fruit)来说,形参为Object类,也就是Fruit的父类,而此时传入的对象只能是Fruit及其子类,Fruit的父类一定是Fruit及其子类的父类,但不能传入Fruit的父类,因为并不知道当前的形参类型(Fruit的父类)与实参类型(Fruit的父类)到底是什么关系。(一个类的两个父类之间关系无法确定)
	public static void main(String[] args) {
        Person<? super Fruit> person = new Person<>();
        person.setFirst(new Fruit());
        person.getFirst();
    }
  而在getXX()上,很明显经过类型擦除后返回的值只能用Object类型来接收,因为根本不知道返回的是Fruit的哪个父类,所以没有一个确定的值,用Object统一接收即可。
  本文参考:泛型上下界通配符
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值