大白话说Java泛型(二):深入理解通配符
文章首发于【博客园-陈树义】,点击跳转到原文《大白话说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泛型(二):深入理解通配符的更多相关文章
- 大白话说Java泛型(一):入门、原理、使用
文章首发于[博客园-陈树义],点击跳转到原文<大白话说Java泛型(一):入门.原理.使用> 远在 JDK 1.4 版本的时候,那时候是没有泛型的概念的.当时 Java 程序员们写集合类的 ...
- 大白话说Java泛型:入门、使用、原理
文章首发于[博客园-陈树义],点击跳转到原文<大白话说Java泛型:入门.使用.原理> 远在 JDK 1.4 版本的时候,那时候是没有泛型的概念的.当时 Java 程序员们写集合类的代码都 ...
- 大白话说Java反射:入门、使用、原理
文章首发于[博客园-陈树义],点击跳转到原文<大白话说Java反射:入门.进阶.原理> 反射之中包含了一个「反」字,所以想要解释反射就必须先从「正」开始解释. 一般情况下,我们使用某个类时 ...
- Java泛型解析(02):通配符限定
Java泛型解析(02):通配符限定 考虑一个这种场景.计算数组中的最大元素. [code01] public class ArrayUtil { public static <T&g ...
- 大白话说Java反射:入门、使用、原理 (转)
文章首发于[博客园-陈树义],点击跳转到原文<大白话说Java反射:入门.进阶.原理> 目录 一个简单的例子 反射常用API 获取反射中的Class对象 通过反射创建类对象 通过反射获取类 ...
- Java泛型二:通配符的使用
原文地址http://blog.csdn.net/lonelyroamer/article/details/7927212 通配符有三种: 1.无限定通配符 形式<?> 2.上边界限定 ...
- Java泛型之上、下界通配符的理解(适合初学)
泛型的由来 为什么需要泛型 Java的数据类型一般都是在定义时就需要确定,这种强制的好处就是类型安全,不会出现像弄一个ClassCastException的数据给jvm,数据安全那么执行的clas ...
- Java 泛型 二
一. 泛型概念的提出(为什么需要泛型)? 首先,我们看下下面这段简短的代码: 1 public class GenericTest { 2 3 public static void main(Stri ...
- JAVA泛型之<? extends T>:(通配符上限)和<? super T>(通配符下限)
一.通配符上限和通配符下限接受的类型 通配符上限:<? extends T> 通配符下限:<? super T> 以下代码是测试结果,注释为解释说明 package xayd. ...
随机推荐
- Wincc flexable的IO域组态
1.题目 2.新建三个变量 3.组态画面,添加IO域1 1)常规设置 2)属性设置 4.组态IO域2 1)常规项 2)属性设置 5.组态第三个IO域 1)常规设置 2)属性设置 6.此外可以设置动画 ...
- 计算机和HMI设备通信之程序上下载
1.传送模式 2.串行接口传送HMI,软件采用Wincc flexable smart v3 3.设置HMI设备,给HMI设备上电 打开控制面板,双击Transfer 使能Enable Channel ...
- ArcGIS API for JavaScript 4.2学习笔记[9] 同一种视图不同数据(Map)同步
本例子核心:对MapView对象的map属性值进行替换即可达到更改地图数据的效果. 这个例子用的不是Map对象了,而是用的发布在服务器上的专题地图(WebMap)来加载到MapView上进行显示. 在 ...
- solr安装配置
1.solr是基于tomcat安装部署的 2.网上下载solr-5.2.1 http://lucene.apache.org/solr/downloads.html 3.解压solr文件 tar zx ...
- Linux下jira自启动设置
jira 的启动主要依靠的是bin目录下的catalina.sh脚本,提供了如init脚本的start,stop等参数----------------------------------------- ...
- tomcat中使用mysql连接池的配置
1.下载相应的jar包,添加到工程中 需要下载的包主要有commons-pool2-2.2 commons-dbcp2-2.0.1-src commons-dbcp2-2.0.1 commons-c ...
- mysql 的 fiter push down 优化
出处:黑洞中的奇点 的博客 http://www.cnblogs.com/kelvin19840813/ 您的支持是对博主最大的鼓励,感谢您的认真阅读.本文版权归作者所有,欢迎转载,但请保留该声明. ...
- Android中Handler的消息处理机制以及源码分析
在实际项目当中,一个很常见的需求场景就是在根据子线程当中的数据去更新ui.我们知道,android中ui是单线程模型的,就是只能在UI线程(也称为主线程)中更新ui.而一些耗时操作,比如数据库,网络请 ...
- StringMVC @RequestParam属性
1.jsp: <a href="springmvc/testRequestParam?username=allen&age=sss">test RequsetP ...
- MicroPython-TPYBoard固件编译教程
本文主要介绍一下,在Linux环境下编译micropython固件的方法和流程. 首先,我们先来看一下MicroPython的源码结构. github地址:https://github.com/mic ...