文章首发于【博客园-陈树义】,点击跳转到原文《大白话说Java泛型(二):深入理解通配符》

上篇文章《大白话说Java泛型(一):入门、原理、使用》,我们讲了泛型的产生缘由以及其基本使用。但泛型还有更加复杂的应用,如:

List<? extends Number> list = new ArrayList();
List<? super Number> list = new ArrayList();

上面的 extends 和 super 关键字其实就是泛型的高级应用:泛型通配符。

但在讲泛型通配符之前,我们必须对编译时类型和运行时类型有一个基本的了解,才能更好地理解通配符的使用。

编译时类型和运行时类型

我们先来看看一个简单的例子。

Class Fruit{}
Class Apple extends Fruit{}

上面声明一个 Fruit 类,Apple 类是 Fruit 类的子类。

接着下面我们声明一个苹果对象:

Apple apple = new Apple();

这样的声明,我相信大家都没有什么异议,声明一个 Apple 类型的变量指向一个 Apple 对象。在上面这段代码中,apple 属性指向的对象,其编译时类型和运行时类型都是 Apple 类型。

但其实很多时候我们也使用下面这种写法:

Fruit apple = new Apple();

我们使用 Fruit 类型的变量指向了一个 Apple 对象,这在 Java 的语法体系中也是没有问题的。因为 Java 允许把一个子类对象(Apple对象)直接赋值给一个父类引用变量(Fruit类变量),一般我们称之为「向上转型」。

那问题来了,此时 apple 属性所指向的对象,其编译时类型和运行时类型是什么呢?

很多人会说:apple 属性指向的对象,其编译时类型和运行时类型不都是 Apple 类型吗?

正确答案是:apple 属性所指向的对象,其在编译时的类型就是 Fruit 类型,而在运行时的类型就是 Apple 类型。

这是为什么呢?

因为在编译的时候,JVM 只知道 Fruit 类变量指向了一个对象,并且这个对象是 Fruit 的子类对象或自身对象,其具体的类型并不确定,有可能是 Apple 类型,也有可能是 Orange 类型。而为了安全方面的考虑,JVM 此时将 apple 属性指向的对象定义为 Fruit 类型。因为无论其是 Apple 类型还是 Orange 类型,它们都可以安全转为 Fruit 类型。

而在运行时阶段,JVM 通过初始化知道了它指向了一个 Apple 对象,所以其在运行时的类型就是 Apple 类型。

泛型中的向上转型

当我们明白了编译时类型和运行时类型之后,我们再来理解通配符的诞生就相对容易一些了。

还是上面的场景,我们有一个 Fruit 类,Apple 类是 Fruit 的子类。这时候,我们增加一个简单的容器:Plate 类。Plate 类定义了盘子一些最基本的动作:

public class Plate<T> {
    private List<T> list;
    public Plate(){}
    public void add(T item){list.add(item);}
    public T get(){return list.get(0);}
}

按我们之前对泛型的学习,我们可以知道上面的代码定义了一个 Plate 类。Plate 类定义了一个 T 泛型类型,可以接收任何类型。说人话就是:我们定义了一个盘子类,这个盘子可以装任何类型的东西,比如装水果、装蔬菜。

如果我们想要一个装水果的盘子,那定义的代码就是这样的:

Plate<Fruit> plate = new Plate<Fruit>();

我们直接定义了一个 Plate 对象,并且指定其泛型类型为 Fruit 类。这样我们就可以往里面加水果了:

plate.add(new Fruit());
plate.add(new Apple());

按照 Java 向上转型的原则,我们当然也觉得 Java 泛型可以向上转型,即我们上面关于水果盘子的定义可以变为这样:

Plate<Fruit> plate = new Plate<Apple>();  //Error

但事实上,这种写法是错误的,上面的代码在编译的时候会出现编译错误。

按理说,这种写法应该是没有问题的,因为 Java 支持向上转型嘛。

错误的原因就是:泛型并不直接支持向上转型,JVM 会要求其指向的对象是 Fruit 类型的对象。

正是为了解决保持「向上转型」概念在 Java 语言中的统一,使泛型也支持向上转型,所以 Java 推出了通配符的概念。

上面这行代码如果要正常编译,只需要修改一下 Plate 类的声明即可:

Plate<? extends Fruit> plate = new Plate<Apple>();

上面的这行代码表示:plate 可以指向任何 Fruit 类对象,或者任何 Fruit 的子类对象。Apple 是 Fruit 的子类,自然就可以正常编译了。

extends通配符的缺陷

虽然通过这种方式,Java 支持了 Java 泛型的向上转型,但是这种方式是有缺陷的,那就是:其无法向 Plate 中添加任何对象,只能从中读取对象。

Plate<? extends Fruit> plate = new Plate<Apple>();
plate.add(new Apple()); //Compile Error
plate.get();    // Compile Success

可以看到,当我们尝试往盘子中加入一个苹果时,会发现编译错误。但是我们可以从中取出东西。那为什么我们会无法往盘子中加东西呢?

这还得从我们对盘子的定义说起。

Plate<? extends Fruit> plate = new Plate<XXX>();

上面我们对盘子的定义中,plate 可以指向任何 Fruit 类对象,或者任何 Fruit 的子类对象。也就是说,plate 属性指向的对象其在运行时可以是 Apple 类型,也可以是 Orange 类型,也可以是 Banana 类型,只要它是 Fruit 类,或任何 Fruit 的子类即可。即我们下面几种定义都是正确的:

Plate<? extends Fruit> plate = new Plate<Apple>();
Plate<? extends Fruit> plate = new Plate<Orange>();
Plate<? extends Fruit> plate = new Plate<Banana>();

这样子的话,在我们还未具体运行时,JVM 并不知道我们要往盘子里放的是什么水果,到底是苹果,还是橙子,还是香蕉,完全不知道。既然我们不能确定要往里面放的类型,那 JVM 就干脆什么都不给放,避免出错。

正是出于这种原因,所以当使用 extends 通配符时,我们无法向其中添加任何东西。

那为什么又可以取出数据呢?因为无论是取出苹果,还是橙子,还是香蕉,我们都可以通过向上转型用 Fruit 类型的变量指向它,这在 Java 中都是允许的。

Fruit apple = plate.get();
Apple apple = plate.get();  //Error

可以从上面的代码看到,当你尝试用一个 Apple 类型的变量指向一个从盘子里取出的水果时,是会提示错误的。

所以当使用 extends 通配符时,我们可以取出所有东西。

总结一下,我们通过 extends 关键字可以实现向上转型。但是我们却失去了部分的灵活性,即我们不能往其中添加任何东西,只能取出东西。

super通配符的缺陷

与 extends 通配符相似的另一个通配符是 super 通配符,其特性与 extends 完全相反。

Plate<? super Apple> plate = new Plate<Fruit>();

上面这行代码表示 plate 属性可以指向一个特定类型的 Plate 对象,只要这个特定类型是 Apple 或 Apple 的父类。也就是说,如果 EatThing 类是 Fruit 的父级,那么下面的声明也是正确的:

Plate<? super Apple> plate = new Plate<EatThing>();

当然了,下面的声明肯定也是对的,因为 Object 是任何一个类的父级。

Plate<? super Apple> plate = new Plate<Object>();

既然这样,也就是说 plate 指向的具体类型可以是任何 Apple 的父级,JVM 在编译的时候肯定无法判断具体是哪个类型。但 JVM 能确定的是,任何 Apple 的子类都可以转为 Apple 类型,但任何 Apple 的父类都无法转为 Apple 类型。

所以对于使用了 super 通配符的情况,我们只能存入 T 类型及 T 类型的子类对象。

Plate<? super Apple> plate = new Plate<Fruit>();
plate.add(new Apple());
plate.add(new Fruit()); //Error

当我们向 plate 存入 Apple 对象时,编译正常。但是存入 Fruit 对象,就会报编译错误。

而当我们取出数据的时候,也是类似的道理。JVM 在编译的时候知道,我们具体的运行时类型可以是任何 Apple 的父级,那么为了安全起见,我们就用一个最顶层的父级来指向取出的数据,这样就可以避免发生强制类型转换异常了。

Object object = plate.get();
Apple apple = plate.get();  //Error
Fruit fruit = plate.get();  //Error

从上面的代码可以知道,当使用 Apple 类型或 Fruit 类型的变量指向 plate 取出的对象,会出现编译错误。而使用 Object 类型的额变量指向 plate 取出的对象,则可以正常通过。

也就是说对于使用了 super 通配符的情况,我们取出的时候只能用 Object 类型的属性指向取出的对象。

PECS原则

说到这里,我相信大家已经明白了 extends 和 super 通配符的使用和限制了。我们知道:

  • 对于 extends 通配符,我们无法向其中加入任何对象,但是我们可以进行正常的取出。
  • 对于 super 通配符,我们可以存入 T 类型对象或 T 类型的子类对象,但是我们取出的时候只能用 Object 类变量指向取出的对象。

从上面的总结可以看出,extends 通配符偏向于内容的获取,而 super 通配符更偏向于内容的存入。我们有一个 PECS 原则(Producer Extends Consumer Super)很好的解释了这两个通配符的使用场景。

Producer Extends 说的是当你的情景是生产者类型,需要获取资源以供生产时,我们建议使用 extends 通配符,因为使用了 extends 通配符的类型更适合获取资源。

Consumer Super 说的是当你的场景是消费者类型,需要存入资源以供消费时,我们建议使用 super 通配符,因为使用 super 通配符的类型更适合存入资源。

但如果你既想存入,又想取出,那么你最好还是不要使用 extends 或 super 通配符。

总结

Java 泛型通配符的出现是为了使 Java 泛型也支持向上转型,从而保持 Java 语言向上转型概念的统一。但与此同时,也导致 Java 通配符出现了一些缺陷,使得其有特定的使用场景。

关于泛型的两篇文章到这里就结束了,希望大家看了都能对泛型有更深入的理解。

文章首发于【博客园-陈树义】,点击跳转到原文《大白话说Java泛型(二):深入理解通配符》

大白话说Java泛型(二):深入理解通配符的更多相关文章

  1. 大白话说Java泛型(一):入门、原理、使用

    文章首发于[博客园-陈树义],点击跳转到原文<大白话说Java泛型(一):入门.原理.使用> 远在 JDK 1.4 版本的时候,那时候是没有泛型的概念的.当时 Java 程序员们写集合类的 ...

  2. 大白话说Java泛型:入门、使用、原理

    文章首发于[博客园-陈树义],点击跳转到原文<大白话说Java泛型:入门.使用.原理> 远在 JDK 1.4 版本的时候,那时候是没有泛型的概念的.当时 Java 程序员们写集合类的代码都 ...

  3. 大白话说Java反射:入门、使用、原理

    文章首发于[博客园-陈树义],点击跳转到原文<大白话说Java反射:入门.进阶.原理> 反射之中包含了一个「反」字,所以想要解释反射就必须先从「正」开始解释. 一般情况下,我们使用某个类时 ...

  4. Java泛型解析(02):通配符限定

    Java泛型解析(02):通配符限定      考虑一个这种场景.计算数组中的最大元素. [code01] public class ArrayUtil { public static <T&g ...

  5. 大白话说Java反射:入门、使用、原理 (转)

    文章首发于[博客园-陈树义],点击跳转到原文<大白话说Java反射:入门.进阶.原理> 目录 一个简单的例子 反射常用API 获取反射中的Class对象 通过反射创建类对象 通过反射获取类 ...

  6. Java泛型二:通配符的使用

    原文地址http://blog.csdn.net/lonelyroamer/article/details/7927212 通配符有三种: 1.无限定通配符   形式<?> 2.上边界限定 ...

  7. Java泛型之上、下界通配符的理解(适合初学)

    泛型的由来 为什么需要泛型   Java的数据类型一般都是在定义时就需要确定,这种强制的好处就是类型安全,不会出现像弄一个ClassCastException的数据给jvm,数据安全那么执行的clas ...

  8. Java 泛型 二

    一. 泛型概念的提出(为什么需要泛型)? 首先,我们看下下面这段简短的代码: 1 public class GenericTest { 2 3 public static void main(Stri ...

  9. JAVA泛型之<? extends T>:(通配符上限)和<? super T>(通配符下限)

    一.通配符上限和通配符下限接受的类型 通配符上限:<? extends T> 通配符下限:<? super T> 以下代码是测试结果,注释为解释说明 package xayd. ...

随机推荐

  1. Linux 学习记录 二 (文件的打包压缩).

     前言:本文参考<鸟哥的Linux 私房菜>,如有说的不对的地方,还请指正!谢谢!  环境:Centos 6.4    和window不同,在Linux压缩文件需要注意的是,压缩后的文件会 ...

  2. Ubuntu中启用ssh服务---转载

    ssh程序分为有客户端程序openssh-client和服务端程序openssh-server.如果需要ssh登陆到别的电脑,需要安装openssh-client,该程序Ubuntu是默认安装的.而如 ...

  3. Java之路第一步——第一行Java代码

    main()方法是Java应用程序的入口方法,也就是说,程序在运行的时候,第一个执行的方法就是main()方法. 名字必须是main: 必须是public static void 类型的: 必须接收一 ...

  4. 防盗链与token运用

    为什么要防盗链? 例如手机/PC应用,如果有人知道你的api地址,和应用格式,那么他人就可以利用这个接口进行盗链:盗取/盗用里面的数据. 防盗链特性: 1.因为是非开放性的,所以所有的接口都是封闭的, ...

  5. 安全扫描工具 Netsparker

    Netsparker是一款web应用安全漏洞扫描工具 Netsparter官网:https://www.netsparker.com/web-vulnerability-scanner/,与其他安全扫 ...

  6. 总结oninput、onchange与onpropertychange事件的用法和区别

    前端页面开发的很多情况下都需要实时监听文本框输入,比如腾讯微博编写140字的微博时输入框hu9i动态显示还可以输入的字数.过去一般都使用onchange/onkeyup/onkeypress/onke ...

  7. TCP协议(二)——TIME_WAIT状态

    当TCP主动关闭套接字时,采用四步握手机制来彻底关闭连接.如图: 客户端主动关闭连接,发送FIN段到服务端.TCP状态由ESTABLISHED(连接状态)转为FIN_WAIT1(表示,发送的FIN需要 ...

  8. 微信小程序开发之选项卡

    选项卡是web开发中经常使用到的一个模块,在小程序中竟然没有,这里参考别人的文章自己做了一个双选项卡 实现思路: 通过绑定swichNav事件来控制currentTab(当前选项卡)和isShow(是 ...

  9. Python核心编程笔记--动态属性

    一.动态语言与静态语言 1.1 静态语言特点: a. 在定义变量时需要指定变量的类型,根据指定的类型来确定变量所占的内存空间 b. 需要经过编译才能运行 c. 在代码编译后,运行过程不能对代码进行操作 ...

  10. 纯CSS二级纵向菜单

    纯CSS二级纵向菜单 <body> <div class="divda"> <div class="nav"> <ul ...