Java 之泛型通配符 ? extends T 与 ? super T 通配符解惑

简述

通配符起到保护的作用,防止误操作,通过idea等编辑器,能够检查出所有的通配符错误!

大家在平时的工作学习中, 肯定会见过不少如下的语句:

List<? super T>
List<? extends T>

我们都知道, 上面的代码时关于 Java 泛型的, 那么这两个不同的写法都有什么区别呢?

首先, 说到 Java 的泛型, 我们必须要提到的是Java 泛型的类型擦除机制: Java中的泛型基本上都是在编译器这个层次来实现的. 在生成的 Java 字节代码中是不包含泛型中的类型信息的. 使用泛型的时候加上的类型参数, 会被编译器在编译的时候去掉. 这个过程就称为类型擦除. 如在代码中定义的List<Object>List<String>等类型, 在编译之后都会变成List, JVM看到的只是List, 而由泛型附加的类型信息对JVM来说是不可见的.

在使用泛型类时:

  • 我们可以使用一个具体的类型, 例如可以定义一个 List<Integer> 的对象, 我们的泛型参数就是 Integer;

  • 我们也可以使用通配符 ? 来表示一个未知类型, 例如 List<?> 就表示了泛型参数是某个类型, 只不过我们并不知道它的具体类型时什么.

     List<?> list= new ArrayList<Object>();  //ok
     
    

    List<?>所声明的就是所有类型都是可以的, 但需要注意的是, List<?>并不等同于List<Object>. 对于 List<Object> 来说, 它实际上确定了 List 中包含的是 Object 及其子类, 我们可以使用 Object 类型来接收它的元素. 相对地, List<?> 则表示其中所包含的元素类型是不确定, 其中可能包含的是 String, 也可能是 Integer. 如果它包含了 String 的话, 往里面添加 Integer 类型的元素就是错误的. 作为对比, 我们可以给一个 List<Object> 添加 String 元素, 也可以添加 Integer 类型的元素, 因为它们都是 Object 的子类.

            List<?> list= new ArrayList<?>();  //编译错误,右侧必须指定一个具体的类型,不能用?
    

    正因为类型未知, 我们就不能通过 new ArrayList<?>() 的方法来创建一个新的ArrayList 对象, 因为编译器无法知道具体的类型是什么. 但是对于 List<?> 中的元素, 我们却都可以使用 Object 来接收, 因为虽然类型未知, 但肯定是Object及其子类.

    • 等号左边可以声明 “?”,并且将来可以用Object类型来接收

    • 右边必须指定一个确定的类型,不能用“?”代替 。

              List<?> list= new ArrayList<Integer>();  //ok,右侧声明一个Integer
              Object o = list.get(0);  //编译通过
              Integer o2 = list.get(0);  //编译失败,不能用Integer来接收
      

我们在上面提到了 List<?> 中的元素只能使用 Object 来引用, 这样作肯定时不太方便的, 不过幸运的是, Java 的泛型机制允许我们对泛型参数的类型的上界和下界做一些限制, 例如

  • List<? extends Number> 定义了泛型的上界是 Number, 即 List 中包含的元素类型是 Number 及其子类.
  • List<? super Number> 定义了泛型的下界, 即 List 中包含的是 Number 及其父类.

当引入了泛型参数的上界和下界后, 我们编写代码相对来说就方便了许多, 不过也引入了新的问题, 即我们在什么时候使用上界, 什么时候使用下界, 以及它们的区别和限制到底时什么? 下面我来说说我的理解.

1. ? extends T

? extends T 描述了通配符上界, 即具体的泛型参数需要满足条件: 泛型参数必须是 T 类型或它的子类, 例如:

List<? extends Number> numberArray = new ArrayList<Number>();  // Number 是 Number 类型的
List<? extends Number> numberArray2 = new ArrayList<Integer>(); // Integer 是 Number 的子类
List<? extends Number> numberArray3 = new ArrayList<Double>();  // Double 是 Number 的子类

上面三个操作都是合法的, 因为 ? extends Number 规定了泛型通配符的上界, 即我们实际上的泛型必须要是 Number 类型或者是它的子类, 而 Number, Integer, Double 显然都是 Number 的子类(类型相同的也可以, 即这里我们可以认为 Number 是 Number 的子类).

? extends T语法,由于无法确定子类型,可能是Integer,那么写入一个Double合适吗?不合适;可能是Double,那么写入一个Integer合适吗?不合适。因此就索性禁止写入了,但是由于知道上界,可以按照 T类型来读

1.1 关于读取,可读

        Number o = numberArray.get(0);  //ok
        Integer o2 = numberArray2.get(0);  //compile error
        Double o3 = numberArray3.get(0);   //compile error 

根据上面的例子, 对于 List<? extends Number> numberArray 对象:

  • 我们能够从 numberArray 中读取到 Number 对象, 因为 numberArray 中包含的元素是 Number 类型或 Number 的子类型.

  • 我们不能从 numberArray 中读取到 Integer 类型, 因为 numberArray 中可能保存的是 Double 类型.

  • 同理, 我们也不能从 numberArray 中读取到 Double 类型.

当然,我们这里讨论的是泛型,即不手动强转,下列语法展示手动强转,可以解决编译错误,但是存在运行时错误:

        Integer o2 = (Integer)numberArray2.get(0);  //compile ok ,run ok
        Double o3 = (Double)numberArray2.get(0);  //compile ok,run error,把numberArray2 Integer类型强转为Double,会在运行时报错

1.2 关于写入,不可写

        List<? extends Number> numberArray2 = new ArrayList<Integer>(); //ok, Integer 是 Number 的子类
        List<? extends Number> numberArray3 = new ArrayList<Double>();  //ok, Double 是 Number 的子类
        numberArray.add(new Integer(0));  //compile error
        numberArray2.add(new Double(0));  //compile error

根据上面的例子, 对于 List<? extends Number> numberArray 对象:

  • 我们不能添加 Number 到 numberArray 中, 因为 numberArray 有可能是List<Double> 类型

  • 我们不能添加 Integer 到 numberArray 中, 因为 numberArray 有可能是 List<Double> 类型

  • 我们不能添加 Double 到 numberArray 中, 因为 numberArray 有可能是 List<Integer> 类型

即, 我们不能添加任何对象到 List<? extends T> 中, 因为我们不能确定一个 List<? extends T> 对象实际的类型是什么, 因此就不能确定插入的元素的类型是否和这个 List 匹配. List<? extends T> 唯一能保证的是我们从这个 list 中读取的元素一定是一个 T 类型的.

2. ? super T

? super T 描述了通配符下界, 即具体的泛型参数需要满足条件: 泛型参数必须是 T 类型或它的父类, 例如:

        List<? super Integer> array = new ArrayList<Integer>(); //ok
        List<? super Integer> array2 = new ArrayList<Number>();//ok
        List<? super Integer> array3 = new ArrayList<Object>();//ok

? super T语法,由于无法确定父类型,因此法无法读定义 A a = array.get(0), (即期望能自动转换的,而不是手动进行类型转换的),但是由于知道下界,可以按照 T来写入

2.1 关于读取

        List<? super Integer> array = new ArrayList<Integer>(); //ok
        List<? super Integer> array2 = new ArrayList<Number>();//ok
        List<? super Integer> array3 = new ArrayList<Object>();//ok
        Integer o =  array.get(0);  //compile error
        Integer o2 =  array.get(0);  //compile error
        Integer o3 =  array.get(0); //compile error

对于上面的例子中的 List<? super Integer> array 对象:

  • 我们不能保证可以从 array 对象中读取到 Integer 类型的数据, 因为 array 可能是 List 类型的.

  • 我们不能保证可以从 array 对象中读取到 Number 类型的数据, 因为 array 可能是 List 类型的.

  • 唯一能够保证的是, 我们可以从 array 中获取到一个 Object 对象的实例.

2.2 关于写

        List<? super Integer> array = new ArrayList<Integer>(); //ok
        List<? super Integer> array2 = new ArrayList<Number>();//ok
        List<? super Integer> array3 = new ArrayList<Object>();//ok
        array.add(new Integer(0));  //ok
        array2.add(new Double(0)); //compile error
        array3.add(new Object()); //compile error

对于上面的例子中的 List<? super Integer> array 对象:

  • 我们可以添加 Integer 对象到 array 中, 也可以添加 Integer 的子类对象到 array 中.

  • 我们不能添加 Double/Number/Object 等不是 Integer 的子类的对象到 array 中.

3. 使用场景

PECE 原则: Producer Extends, Consumer Super

从上述两方面的分析,总结PECS原则如下:

  • 如果要从集合中读取类型T的数据,并且不能写入,可以使用 ? extends 通配符;(Producer Extends)
  • 如果要从集合中写入类型T的数据,并且不需要读取,可以使用 ? super 通配符;(Consumer Super)
  • 如果既要存又要取,那么就不要使用任何通配符。

例子,Collections的源码,在copy方法中的定义,通配符起到保护的作用,防止误操作:

public class Collections { 
  public static <T> void copy(List<? super T> dest, List<? extends T> src) 
  {
      for (int i=0; i<src.size(); i++) 
        dest.set(i,src.get(i)); 
  } 
}

参考

Java 之泛型通配符 ? extends T 与 ? super T 解惑

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值