泛型的本质是 类型参数化,解决类型爆炸的问题。
所谓泛型是指将类型参数化,以达到代码复用提高软件开发工作效率的一种数据类型。
然后我们要定义一个盘子 plate,注意这个盘子除了 装入食物food之外,还可以装其他的比如 小玩具。
为了装不同类型的食物,我们需要定义不同的盘子:
(1) 装水果的盘子 FruitPlate
(2) 装肉的盘子 MeatPlate
(3) 装苹果的盘子 ApplePlate
(4) 装香蕉的盘子 BananaPlate
.....
(N) 装云南苹果的盘子 YunnanFruitPlate
这就是盘子类型的 类型爆炸。
如何解决上面的类型爆炸问题呢? 这就要用到泛型。
那么盘子里的东西的类型,我们就用泛型
从这个例子看到:泛型是一种类型占位符,或称之为类型参数。
如何使用呢?
public static void main(String[] args) {
//创建一个装水果的盘子
PlateDemo1 plateDemo2 =new PlateDemo1<>(new Apple());
}
所谓泛型,就是 数据类型 指定为一个参数,在不创建新类的情况下,通过创建变量的时候去确定 数据的具体类型。
也就是说,在创建对象或者调用方法的时候才明确下具体的类型。
泛型可以在类、接口、方法中使用,分别称为泛型类、泛型接口、泛型方法。
修饰符 class 类名<类型> { }
上面的例子就是 泛型类
class PlateDemo1 {
//盘子里的东西
private T someThing;
}
定义格式:
修饰符 <泛型类型> 返回值类型 方法名(类型 变量名) { }
示例代码:
public void demo(T t) {
...
}
定义格式:
修饰符 interface 接口名<类型> { }
示例代码:
public interface Generic {
void demo(T t);
}
泛型接口的实现类
public class GenericImpl implements Generic {
public void demo(T t) {
...
}
}
没有泛型的情况的下,好像Object也能实现简单的 泛化。
通过定义为类型Object的引用,来实现参数的“任意化”。
比如上面的例子的 泛型类
通过定义为类型Object的引用,来实现参数的“任意化”,结果如下
class PlateDemo1 {
//盘子里的东西
private Object someThing;
}
Object实现参数的 “泛型化”、“任意化”带来的缺点是:要做显式的强制类型转换。
参数类型强制转换有一个大大降低代码复用性和扩展性的坏处:
首先,要求开发者对实际参数类型可预知。
其次,不利于未来的 扩展。
1、避免了强制类型转换,提高代码的复用性和扩展性
泛型中,所有的类型转换都是自动和隐式的,不需要强制类型转换,可以提高代码的重用率,再加上明确的类型信息,代码的可读性也会更好。
2、把运行时期的问题提前到了编译期,编译时的类型检查,使程序更加健壮
使用普通的Object泛化,对于强制类型转换错误的情况,编译期不会提示错误,在运行的时候才出现异常,这是一个安全隐患。
泛型的好处是在编译期检查类型安全,并能捕捉类型不匹配的错误,避免运行时抛出类型转化异常ClassCastException,将运行时错误提前到编译时错误,消除安全隐患。
正是由于以上两点原因,泛型得到了广泛的应用。
比如Java中,所有的标准集合接口都是泛型化的:Collection
、List
、Set
和 Map
。
现在我定义一个“水果盘子”,用来装苹果, 逻辑上水果盘子当然可以装苹果。
那么,一个“装苹果的盘子”,能转换成一个“装水果的盘子”吗?
看下面的例子
那么,一个“装苹果的盘子”,能转换成一个“装水果的盘子”吗? 答案是不行的。
编译器 的逻辑是这样的:
苹果 is-a 水果
装苹果的盘子 not is-a 装水果的盘子
也就是说:就算 苹果 is-a 水果,但容器之间是没有继承关系的。
怎么办?这里用到了 泛型上界。 泛型上界是这么定义的:
<?extends 基类B>
<?extends 基类B>
表示泛型实参类型的上界是“基类B”,
换句话说,泛型实参的类型,可能是“基类B” 或者是“基类B”的子类;
修改之后的例子如下,使用 泛型上界通配符(Upper Bounds Wildcards)后,编译器就不报错误了:
使用(Upper Bounds Wildcards)通配符作为泛型实参,所定义 PlateDemo1<?extends Fruit>
引用,可以 覆盖下图中方框内部的所有子类的 泛型对象。
<?extends T>
表示类型的上界,参数化类型可能是T 或者是 T的子类;
PlateDemo1<?extends Fruit>
引用,可以 覆盖下图中方框内部的所有子类的 泛型对象,编译器都不报错,下面的代码如下:
为啥<?extends Fruit>
叫做 上界,而不叫下届? 原因是:这个通配符,定义了实参的类型上限 为 Fruit,具体如下图:
上界通配符(Upper Bounds Wildcards)的问题
上界通配符(Upper Bounds Wildcards)的作用,实现了 子类泛型对象 到 父类Java泛型对象之间的引用转换。
但是,这样的引用转换也有一定的副作用。
具体如下:
通过例子可以看到:
(1)往基类盘子,set( ) 任何对象,都 失效了
(2)从基类盘子,get ( ) 对象的引用,返回 类型是上界对象, 这个还是 可以的
简单来说:上界 extends T>
不能往里存,只能往外取
所以,上界通配符(Upper Bounds Wildcards)什么时候用,什么时候不用呢:
(1)当从集合中获取元素进行操作的时候用,可以用当前元素的类型接收,也可以用当前元素的父类型接收。
(2)往集合中添加元素时,不能用上界通配符(Upper Bounds Wildcards)。
往集合中添加元素时,不能用上界通配符(Upper Bounds Wildcards)。
怎么办呢?
Java也提供了一种通配符,叫做 泛型的下界/ 下界通配符(Lower Bounds Wildcards)。
泛型上界是这么定义的:
<?super 子类C>
<?super 子类C>
表示泛型实参类型的下界是“子类C”,
super T>
表示 T是类型下边界,参数化类型是此T类型的超类型,直至object;
下界/ 下界通配符(Lower Bounds Wildcards)的问题
下界/ 下界通配符(Lower Bounds Wildcards) 作用,实现了 复类泛型对象 到 子类Java泛型对象之间的引用转换。
但是,这样的引用转换也有一定的副作用。
具体如下:
通过例子可以看到:
(1)往基类盘子,set( ) 任何子类对象,都是OK的
(2)从基类盘子,get ( ) 对象的引用是编译错误的,除非是Object类型
简单来说:下界 super T>
可以往里存,但不能向外取,要取只能取Object对象
所以,下界/ 下界通配符(Lower Bounds Wildcards)什么时候用,什么时候不用呢:
(1)当往集合中添加元素时候用,既可以添加T类型对象,又可以添加T的子类型对象
(2)当从集合get ( ) 对象的引用时,不能用上界通配符(Upper Bounds Wildcards)。除非get 的是Object类型
PECS原则的全称是Producer Extends Consumer Super
,很多小伙伴从没听说过,面试的时候,只要面试官一问,大部分都是一脸懵逼。
什么是PECS(Producer Extends Consumer Super)原则?PECS原则全称"Producer Extends, Consumer Super",即上界生产,下界消费。
Producer Extends 上界生产,就是 生产者使用 “? extends T”通配符。
Consumer Super 下界消费,就是消费者使用 “? super T”通配符
最终PECS (Producer Extends Consumer Super ) 原则
频繁往外读取内容的,适合用上界Extends。
经常往里插入的,适合用下界Super。
Producer Extends 上界生产,就是 生产者使用 “? extends T”通配符。
以“? extends T”声明的集合,不能往此集合中添加元素,所以它也只能作为生产者
所以,使用 “? extends T” 上界,能轻松地成为 producer 生产者,完成
读取元素
迭代元素
这就是 Producer Extends 上界生产,就是 生产者使用 “? extends T”通配符。
Consumer Super 下界消费,就是消费者使用 “? super T”通配符
在通配符的表达式中,只有“? super T”能添加元素,所以它能作为消费者(消费其他通配符集合)。
当然,针对采用“? super T”通配符的集合,对其遍历时需要多一次转型。
总之 PECS就是:
1、频繁往外读取内容的,适合用上界Extends。
2、经常往里插入的,适合用下界Super
明白了泛型、泛型的上界,泛型的下届之后,带大家来回答这个面试的核心问题:什么是泛型的擦除。
前面讲到,泛型的本质是 类型参数化,解决类型爆炸的问题。比如:如果我们的代码中存在很多的 食物类型, 继承关系如下
没有泛型,为了实现去装不同类型的食物,我们需要定义不同的盘子:
(1) 装水果的盘子 FruitPlate
(2) 装肉的盘子 MeatPlate
(3) 装苹果的盘子 ApplePlate
(4) 装香蕉的盘子 BananaPlate
.....
(N) 装云南苹果的盘子 YunnanFruitPlate
如何解决上面的类型爆炸问题呢? 这就要用到泛型。
而使用泛型,我们定义一个就可以了:
class PlateDemo1 {
//盘子里的东西
private T someThing;
public PlateDemo1(T t) {
someThing = t;
}
....
}
这样,就避免 了 盘子类型的 类型爆炸。尤其在Java中的集合类,如果不用泛型,不知道要定义多少的具体集合类。
那么 Java中的泛型,有一个 类型擦除 的特点:
java的泛型,只在编译期有效。
编译之后的字节码,已经抹除了泛型信息。
所谓的类型擦除(type erasure)
,指的是泛型只在编译时起作用,在进入JVM之前,泛型会被擦除掉,根据泛型定义的形式而被替换为相应的类型。这也说明了Java的泛型其实是伪泛型。
类型擦除简单来说,泛型类型在逻辑上可以看成是多个不同的类型,实际上都是相同类型。
比如:
Food food = new Fruit(); // 没问题
ArrayList list= new ArrayList(); // 报错
或者说下面的ArrayList ,在逻辑上看,可以看成是多个不同的类型,实际上都是相同类型
看下面的例子
类型参数在运行中并不存在,这意味着:
运行期间,泛型不会添加任何的类型信息;
不能依靠泛型参数,进行类型转换。
Java泛型的实现是靠类型擦除技术实现的,类型擦除是在编译期完成的,泛型擦除怎么做呢?
在编译期,编译器会将泛型的类型参数都擦除成它指定的原始限定类型
如果没有指定的原始限定类型则擦除为Object类型,之后在获取的时候再强制类型转换为对应的类型,
因此生成的Java字节码中是不包含泛型中的类型信息的,即运行期间并没有泛型的任何信息。
当泛型类型被声明为一个具体的泛型标识,或一个无界通配符
时,泛型类型将会被替代为Object
。
这也比较容易理解,如 List>,PlateDemo1>
, 当获取元素的时,因为不能够确定具体的类型,所以只能使用Object
来接收,
在擦除的时候也是一样的道理,无法确定具体类型,所以擦除泛型时会将其替换为Object
类型,如:
当泛型类型被声明为一个上界通配符
时,泛型类型将会被替代为相应上界的类型。
主要,这里的上界,指的是用于类型定义场景里边的上界:
而不是变量定义场景里边用到到泛型上界,如下:
List extends Fruit> producer =...;
用泛型上界定义class的时候,指的是用于类型定义,泛型类型将会被替代为相应上界的类型。
下界通配符
的擦除,同无界通配符
,
下届只能定义引用的时候用,在定义类型的时候用不了,所以下界擦除只能替换为Object
。
下界擦除只能替换为Object
。