泛型中上界与下界


jdk部分源码中<? extends T>和<? super T>,一直不太明白是什么意思,针对该部分来系统学习下


一、出现前景

先看下面4个类,继承关系如下:
在这里插入图片描述

class A{}

class B extends A{}

class C extends B{}

class D extends A{}

假设有testArr(A[] obj)和testGeneri(A a),针对该方法,形参为A,调用是可以传入A或者A的子类,如下所示都可以正常编译。

public void test(){
   testArr(new A[10]);  //编辑成功,执行成功
   testArr(new B[10]);  //编辑成功,执行成功
   testArr(new C[10]);  //编译成功,执行报错
   testArr(new D[10]);  //编译成功,执行报错
    
   testGeneri(new A());
   testGeneri(new B());
   testGeneri(new C());
   testGeneri(new D());
}

private void testArr(A[] obj){
	obj[0] = new B();
}

private void testGeneri(A a){}

假如换个需求,针对List集合的形参,方法如下所示:

private void testGeneriList(List<A> a){
	//D是A的子类,这条语句能正常执行
    a.add(new D());
}

在这里插入图片描述

      如果在调用的时候,传入的是泛型为B对象的list集合,则报错如上所示,第二条语句乍一看感觉是没有问题的,因为B继承至A,在需要List<A>的地方传入List<B>,却发现这里编译不通过。
对于数组来说,数组是协变的,也就是说对于B extends A,要求A[ ]的地方可以传入B[ ],即A是B的父类,A[ ]也是B[ ]的父类,但是这个规则对于泛型是不适用的,泛型并不是协变的。

为什么对于集合来说,不适用于这条规则呢?------泛型擦除
我们知道的是 Java 中的泛型基本上都是在编译器这个层次来实现的。在生成的 Java 字节代码中是不包含泛型中的类型信息的。使用泛型的时候加上的类型参数,会被编译器在编译的时候去掉。这个过程就称为类型擦除。如在代码中定义的 List<Object> 和 List<String> 等类型,在编译之后都会变成 List。JVM 看到的只是 List,而由泛型附加的类型信息对 JVM 来说是不可见的。
类型擦除过程简单来看就是:首先是找到用来替换类型参数的具体类。这个具体类一般是 Object。如果指定了类型参数的上界的话,则使用这个上界,否则就是Object。

       通过上面的分析可以看出,List<String>并不继承于List<Object>,但是对于某些地方,比如上面的testGeneriList方法来说,为了提高复用性,形参为List<A>,只能传入List<A>或者ArrayList<A>,无法使用具体的泛型对象,如List<B>。
针对该类问题,引入了?通配符,并通过<? extends T>和<? super T>确定其上下界限。

二、通配符

?代表的是未知类型,List<?>表示List集合中的元素是未知的,即可能是String,也可能是Integer。但是需要注意的是List<?>并不是等同于List<Object>。不能使用new ArrayList<?>()来创建对象,因为编译器并不知道里面的具体类型。

引入泛型之后的类型系统增加了两个维度:一个是类型参数自身的继承体系结构,另外一个是泛型类或接口自身的继承体系结构。第一个指的是对于 List<String> 和 List<Object> 这样的情况,类型参数 String 是继承自 Object 的。而第二种指的是 List 接口继承自 Collection 接口。对于这个类型系统,有如下的一些规则:

  • 相同类型参数的泛型类的关系取决于泛型类自身的继承体系结构。即 List<String> 是 Collection<String> 的子类型,List<String> 可以替换 Collection<String>。这种情况也适用于带有上下界的类型声明
  • 当泛型类的类型声明中使用了通配符的时候, 其子类型可以在两个维度上分别展开。如对 Collection<? extends Number> 来说,其子类型可以在 Collection 这个维度上展开,即 List<? extends Number> 和 Set<? extends Number> 等;也可以在 Number 这个层次上展开,即 Collection<Double> 和 Collection<Integer> 等。如此循环下去,ArrayList<Long> 和 HashSet<Double> 等也都算是 Collection<? extends Number> 的子类型。
  • 如果泛型类中包含多个类型参数,则对于每个类型参数分别应用上面的规则。

三、上界 <? extends T>

限定了泛型范围为T和T的子类,下面代码均能正常执行

    public void test1(){
        test3(new ArrayList<A>()); //编译成功
        test3(new ArrayList<B>()); //编译成功
        test3(new ArrayList<C>()); //编译成功
        test3(new ArrayList<D>()); //编译成功
    }


    public void test3(List<? extends A> list){
    }

看到这,我们不难想到一个问题,假如针对test3方法来说,lsit存放的是A或者A的子类,如果我们在该方法中加入下面的代码:
在这里插入图片描述
list里存放的是A或者A的子类,假设上面的代码能编译通过,如果调用test3方法的地方,传入的是List<C>,则在方法中的add方法添加的是一个B类型的对象,泛型为C的集合添加B类型的对象是错误的。也就是说编译器会尽可能的检查可能存在的类型安全问题,对于确定是违反相关原则的地方,会给出编译错误,当编译器无法判断类型的使用是否正确的时候,会给出警告信息。

但是对于从集合中取值,取得都是A或者A的子类对象,可以直接用A接收,如下:
在这里插入图片描述
所以针对于上界来说,只能取,不能存

三、下界 <? super T>

同理,下界就是限定了范围为T和T的父类的所有类型,如下:

    public void test1(){
        test4(new ArrayList<A>()); //编译成功
        test4(new ArrayList<B>()); //编译成功
        test4(new ArrayList<C>()); //编译成功
        test4(new ArrayList<D>()); //编译报错*******
    }

    public void test4(List<? super C> list){
    }

如下图:对于下界来说,list里存的都是B或者B的父类,往list中添加的时候,可以存入B,但是却不能存入B的父类型A,看到这里我们可能会有个疑问,list不是限定了泛型为B或者B的父类,A是满足条件的啊,为啥编译器这里报错了,其实这里也好理解,类比于上界不能存的例子,我们这里假设list.add(new A())能正常编译通过,在调用test4的地方如果传入了一个List<B>,那么在方法中,一个B类型集合却添加了A类型对象,即使A为B的父类,这是不合法的,编译器检查到了这里存在类型安全问题,违反了相关原则,给出编译错误。
list存入的都是B的父类,也就是说B是当前list集合中最小的单元,无论传入B本身,还是B的父类亦或者是B的父类的父类,都可以添加B对象,所以上界这里能存,但是只能存入最小泛型对象。
在这里插入图片描述
上界取值,由于list是B的父类,那么最顶层就是Object,所有可以取值,但是取出来Object类型:

在这里插入图片描述

四、PECS原则

通过上面的例子我们能得出结论:

  • 上界<? extends T>不能存,只能往外取,取出来的东西只能放到T或T的父类中。
  • 下界<? super T>可以往里存,但往外取只能放在Object对象里。

回看上面那句话:
编译器会尽可能的检查可能存在的类型安全问题,对于确定是违反相关原则的地方,会给出编译错误,当编译器无法判断类型的使用是否正确的时候,会给出警告信息。

这个原则就是PESC((Producer Extends Consumer Super))原则:

  • Producer Extends 生产者使用Extends来确定上界,往里面放东西来生产
  • Consumer Super 消费者使用Super来确定下界,往外取东西来消费。

(上界生产,下界消费)理解起来就是:
      如果在一个情景下限制只能从里面取数据(生产者),那么可以使用上界,这样取出来的对象都是T或者T的子类,通过向上转型也都可以用T接收;
      如果想限制只能向list里面写数据(消费者),那么可以使用下界,这样存入的数据都是T或者T的父类,无论对象实际类型是什么,都可以向里面写入T类型对象。

总结:

1、频繁往外读取内容的,适合用上界Extends,即extends 可用于返回类型限定,不能用于参数类型限定。

2、经常往里插入的,适合用下界Super,super 可用于参数类型限定,不能用于返回类型限定。

3、带有 super 超类型限定的通配符可以向泛型对象用写入,带有 extends 子类型限定的通配符可以向泛型对象读取

参考文章:

https://blog.csdn.net/qq_45545968/article/details/122464496
https://blog.csdn.net/jdsjlzx/article/details/70479227

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值