资讯专栏INFORMATION COLUMN

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

woshicixide / 2504人阅读

摘要:简述大家在平时的工作学习中肯定会见过不少如下的语句我们都知道上面的代码时关于泛型的那么这两个不同的写法都有什么区别呢首先说到的泛型我们必须要提到的是泛型的类型擦除机制中的泛型基本上都是在编译器这个层次来实现的在生成的字节代码中是不包含泛型中

简述

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

List
List

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

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

在使用泛型类时, 我们可以使用一个具体的类型, 例如可以定义一个 List 的对象, 我们的泛型参数就是 Integer; 我们也可以使用通配符 ? 来表示一个未知类型, 例如 List 就表示了泛型参数是某个类型, 只不过我们并不知道它的具体类型时什么.
List所声明的就是所有类型都是可以的, 但需要注意的是, List并不等同于List. 对于 List 来说, 它实际上确定了 List 中包含的是 Object 及其子类, 我们可以使用 Object 类型来接收它的元素. 相对地, List 则表示其中所包含的元素类型是不确定, 其中可能包含的是 String, 也可能是 Integer. 如果它包含了 String 的话, 往里面添加 Integer 类型的元素就是错误的. 作为对比, 我们可以给一个 List 添加 String 元素, 也可以添加 Integer 类型的元素, 因为它们都是 Object 的子类.
正因为类型未知, 我们就不能通过 new ArrayList() 的方法来创建一个新的ArrayList 对象, 因为编译器无法知道具体的类型是什么. 但是对于 List 中的元素, 我们却都可以使用 Object 来接收, 因为虽然类型未知, 但肯定是Object及其子类.

我们在上面提到了, List 中的元素只能使用 Object 来引用, 这样作肯定时不太方便的, 不过幸运的是, Java 的泛型机制允许我们对泛型参数的类型的上界和下界做一些限制, 例如 List 定义了泛型的上界是 Number, 即 List 中包含的元素类型是 Number 及其子类. 而 List 定义了泛型的下界, 即 List 中包含的是 Number 及其父类.
当引入了泛型参数的上界和下界后, 我们编写代码相对来说就方便了许多, 不过也引入了新的问题, 即我们在什么时候使用上界, 什么时候使用下界, 以及它们的区别和限制到底时什么? 下面我来说说我的理解.

? extends T

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

List numberArray = new ArrayList();  // Number 是 Number 类型的
List numberArray = new ArrayList(); // Integer 是 Number 的子类
List numberArray = new ArrayList();  // Double 是 Number 的子类

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

子类型判断

假设有类型 G, 以及 SuperClass 和 SubClass 两个类, 并且 SuperClass 是 SubClass 的父类, 那么:

G 是 G 的子类型. 如 List 是 List 的子类型

G 是 G 的子类型, 例如 List 是 List 的子类型.

G 和 G 等同.

可以想象 G 为一个左闭右开的区间(T 在最左边), G 是最大的区间, 当区间 G 包含 区间 G时, 那么较大的区间就是父类.

关于读取

根据上面的例子, 对于 List numberArray 对象:

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

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

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

关于写入

根据上面的例子, 对于 List numberArray 对象:

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

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

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

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

? super T

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

// 在这里, Integer 可以认为是 Integer 的 "父类"
List array = new ArrayList();
// Number 是 Integer 的 父类
List array = new ArrayList();
// Object 是 Integer 的 父类
List array = new ArrayList();
关于读取

对于上面的例子中的 List array 对象:

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

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

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

关于写

对于上面的例子中的 List array 对象:

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

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

易混淆点

有一点需要注意的是, ListList 中, 我们所说的 XX 是 T 的父类(a superclass of T)XX 是 T 的子类(a subclass of T) 其实是针对于泛型参数而言的. 例如考虑如下例子:

List l1 = ...
List l2 = ...

那么这里 ? super Integer? extends Integer 的限制是对谁的呢? 是表示我们可以插入任意的对象 X 到 l1 中, 只要 X 是 Integer 的父类? 是表示我们可以插入任意的对象 Y 到 l2 中, 只要 Y 是 Integer 的子类?
其实不是的, 我们必须要抛弃上面的概念, ? super Integer? extends Integer 限制的其实是 泛型参数, 即 List l1 表示 l1 的泛型参数 T 必须要满足 T 是 Integer 的父类, 因此诸如 List, List 的对象就可以赋值到 l1 中. 正因为我们知道了 l1 中的泛型参数的边界信息, 因此我们就可以向 l1 中添加 Integer 对象了, 推理过程如下:

令 T 是 l1 的泛型参数, 即:
    l1 = List = List
因此有 T 是 Integer 或 Integer 的父类.
如果 T 是 Integer, 则 l1 = List, 显然我们可以添加任意的 Integer 对象或 Integer 的子类对象到 l1 中.
如果 T 是 Integer 的父类, 那么同理, 对于 Integer 或 Integer 的子类的对象, 我们也可以添加到 l1 中.

按同样的分析方式, List l2 表示的是 l2 的泛型参数是 Integer 的子类型. 而如果我们要给一个 List 插入一个元素的话, 我们需要保证此元素是 T 或是 T 的子类, 而这里 List l2, l2 的泛型参数是什么类型我们都不知道, 进而就不能确定 l2 的泛型参数的子类是哪些, 因此我们就不能向 l2 中添加任何的元素了.

来一个对比:

对于 List l1:

正确的理解: ? super Integer 限定的是泛型参数. 令 l1 的泛型参数是 T, 则 T 是 Integer 或 Integer 的父类, 因此 Integer 或 Integer 的子类的对象就可以添加到 l1 中.

错误的理解: ? super Integer限定的是插入的元素的类型, 因此只要是 Integer 或 Integer 的父类的对象都可以插入 l1 中

对于 List l2:

正确的理解: ? extends Integer 限定的是泛型参数. 令 l2 的泛型参数是 T, 则 T 是 Integer 或 Integer 的子类, 进而我们就不能找到一个类 X, 使得 X 是泛型参数 T 的子类, 因此我们就不可以向 l2 中添加元素. 不过由于我们知道了泛型参数 T 是 Integer 或 Integer 的子类这一点, 因此我们就可以从 l2 中读取到元素(取到的元素类型是 Integer 或 Integer 的子类), 并可以存放到 Integer 中.

错误的理解: ? extends Integer 限定的是插入元素的类型, 因此只要是 Integer 或 Integer 的子类的对象都可以插入 l2 中

使用场景

PECE 原则: Producer Extends, Consumer Super

Producer extends: 如果我们需要一个 List 提供类型为 T 的数据(即希望从 List 中读取 T 类型的数据), 那么我们需要使用 ? extends T, 例如 List. 但是我们不能向这个 List 添加数据.

Consumer Super: 如果我们需要一个 List 来消费 T 类型的数据(即希望将 T 类型的数据写入 List 中), 那么我们需要使用 ? super T, 例如 List. 但是这个 List 不能保证从它读取的数据的类型.

如果我们既希望读取, 也希望写入, 那么我们就必须明确地声明泛型参数的类型, 例如 List.

例子:

public class Collections { 
  public static  void copy(List dest, List src) 
  {
      for (int i=0; i

上面的例子是一个拷贝数据的代码, src 是 List 类型的, 因此它可以读取出 T 类型的数据(读取的数据类型是 T 或是 T 的子类, 但是我们不能确切的知道它是什么类型, 唯一能确定的是读取的类型 is instance of T), , dest 是 List 类型的, 因此它可以写入 T 类型或其子类的数据.

参考

Java深度历险(五)——Java泛型
difference-between-super-t-and-extends-t-in-java

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/66693.html

相关文章

  • Java系列泛型

    摘要:总结泛型的类型必须是引用类型,不能是基本类型,泛型的个数可以有多个,可以使用对创建对象时的泛型类型以及方法参数类型进行限制,如使用关键字和对泛型的具体类型进行向下限制或向上限制,最后一点,可以声明泛型数组,但是不能创建泛型数组的实例。 自从 JDK 1.5 提供了泛型概念,泛型使得开发者可以定义较为安全的类型,不至于强制类型转化时出现类型转化异常,在没有反省之前,可以通过 Object...

    MadPecker 评论0 收藏0
  • 泛型泛型

    摘要:定义具有一个或多个类型变量的类,称之为泛型类。泛型类的继承创建对象的两种方式错误方式错误原因继承了泛型类,但并不是泛型类,所以不能这样创建对象。同样是泛型类,它的父类也是泛型类,它传递的是常量。 泛型类 public class A { //在成员变量上使用泛型 private T t; public A() {} //构造参数类型上...

    caoym 评论0 收藏0
  • java编程思想》—— 泛型

    摘要:引用泛型除了方法因不能使用外部实例参数外,其他继承实现成员变量,成员方法,方法返回值等都可使用。因此,生成的字节码仅包含普通的类,接口和方法。 为什么要使用泛型程序设计? 一般的类和方法,只能使用具体的类型:要么是基本类型,要么是自定义类的对应类型;如果要编写可以应用于多种类型的代码,这种刻板的限制对代码的束缚就会很大。----摘自原书Ordinary classes and meth...

    CODING 评论0 收藏0
  • Java™ 教程(类型擦除)

    类型擦除 泛型被引入到Java语言中,以便在编译时提供更严格的类型检查并支持通用编程,为了实现泛型,Java编译器将类型擦除应用于: 如果类型参数是无界的,则用它们的边界或Object替换泛型类型中的所有类型参数,因此,生成的字节码仅包含普通的类、接口和方法。 如有必要,插入类型转换以保持类型安全。 生成桥接方法以保留扩展泛型类型中的多态性。 类型擦除确保不为参数化类型创建新类,因此,泛型不会...

    zsy888 评论0 收藏0
  • Java知识点总结(Java泛型

    摘要:知识点总结泛型知识点总结泛型泛型泛型就是参数化类型适用于多种数据类型执行相同的代码泛型中的类型在使用时指定泛型归根到底就是模版优点使用泛型时,在实际使用之前类型就已经确定了,不需要强制类型转换。 Java知识点总结(Java泛型) @(Java知识点总结)[Java, Java泛型] [toc] 泛型 泛型就是参数化类型 适用于多种数据类型执行相同的代码 泛型中的类型在使用时指定 泛...

    linkin 评论0 收藏0

发表评论

0条评论

最新活动
阅读需要支付1元查看
<