写在前面:安全
泛型的目的就是为了规范参数app
1·泛型就是将类型参数化,其在编译时才肯定具体的参数。学习
2·泛型只存在于编译阶段,而不存在于运行阶段ui
其实正常开发中不多用到下面的写法,作一个了解就能够了code
泛型除了咱们最基本的使用,还有更加复杂的应用,如:对象
List<? extends Number> list = new ArrayList(); List<? super Number> list = new ArrayList();
上面的 extends 和 super 关键字其实就是泛型的高级应用:泛型通配符。资源
但在讲泛型通配符以前,咱们必须对编译时类型和运行时类型有一个基本的了解,才能更好地理解通配符的使用。开发
编译时类型和运行时类型get
咱们先来看看一个简单的例子。it
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 通配符更偏向于内容的存入。咱们有一个 PECS 原则(Producer Extends Consumer Super)很好的解释了这两个通配符的使用场景。
Producer Extends 说的是当你的情景是生产者类型,须要获取资源以供生产时,咱们建议使用 extends 通配符,由于使用了 extends 通配符的类型更适合获取资源。
Consumer Super 说的是当你的场景是消费者类型,须要存入资源以供消费时,咱们建议使用 super 通配符,由于使用 super 通配符的类型更适合存入资源。
但若是你既想存入,又想取出,那么你最好仍是不要使用 extends 或 super 通配符。
总结
Java 泛型通配符的出现是为了使 Java 泛型也支持向上转型,从而保持 Java 语言向上转型概念的统一。但与此同时,也致使 Java 通配符出现了一些缺陷,使得其有特定的使用场景。