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