在40岁老架构师 尼恩的读者交流群(50+)中,最近有小伙伴拿到了一线互联网企业如阿里、滴滴、极兔、有赞、希音、百度、网易、美团的面试资格,遇到很多很重要的面试题:
问题1:什么是PECS原则? 说说具体怎么用?
问题2:什么是 泛型擦除? 说说原理?
问题3:什么是泛型上界? 什么是泛型下界?
最近又有小伙伴在面试美团,遇到了相关的面试题。
很多小伙伴说,自己对什么PECS 原则,可以说一脸懵逼,面试官不满意,面试挂了。
借着此文,尼恩给大家做一下系统化、体系化的梳理,使得大家内力猛增,展示一下雄厚的 “技术肌肉、技术实力”,让面试官爱到 “不能自已、口水直流”,然后实现”offer直提,offer自由”。
当然,这道面试题,以及参考答案,也会收入咱们的 《尼恩Java面试宝典PDF》V162版本,供后面的小伙伴参考,提升大家的 3高 架构、设计、开发水平。
《尼恩 架构笔记》《尼恩高并发三部曲》《尼恩Java面试宝典》的PDF,请到文末公号【技术自由圈】取
泛型的本质是 类型参数化,解决类型爆炸的问题。
所谓泛型是指将类型参数化,以达到代码复用提高软件开发工作效率的一种数据类型。
比如: 如果我们的代码中存在很多的 食物类型, 继承关系如下
然后我们要定义一个盘子 plate,注意这个盘子除了 装入食物food之外,还可以装其他的比如 小玩具。
为了装不同类型的食物,我们需要定义不同的盘子:
(1) 装水果的盘子 FruitPlate
(2) 装肉的盘子 MeatPlate
(3) 装苹果的盘子 ApplePlate
(4) 装香蕉的盘子 BananaPlate
.....
(N) 装云南苹果的盘子 YunnanFruitPlate
这就是盘子类型的 类型爆炸。
如何解决上面的类型爆炸问题呢? 这就要用到泛型。
那么盘子里的东西的类型,我们就用泛型
//盘子里的东西
private T someThing;
从这个例子看到:泛型是一种类型占位符,或称之为类型参数。
如何使用呢?
public static void main(String[] args) {
//创建一个装肉的盘子
PlateDemo1<Meat> plateDemo1 =new PlateDemo1<>(new Pork());
//创建一个装水果的盘子
PlateDemo1<Fruit> plateDemo2 =new PlateDemo1<>(new Apple());
}
所谓泛型,就是 数据类型 指定为一个参数,在不创建新类的情况下,通过创建变量的时候去确定 数据的具体类型。
也就是说,在创建对象或者调用方法的时候才明确下具体的类型。
泛型定义格式:
<类型>:指定一种类型的格式,这里的类型可以看做是形参
<类型1,类型2…>:指定多种类型的格式,多种类型之间用逗号隔开。定义的时候是泛型形参
这个泛型形参,将来具体调用时候,需要有给定的类型,那个给定的具体的Java类型可以看出是实参。
泛型可以在类、接口、方法中使用,分别称为泛型类、泛型接口、泛型方法。
第一类:泛型类
定义格式:
修饰符 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的引用,来实现参数的“任意化”。
比如上面的例子的 泛型类
//盘子,可以装 任何东西,包括 食物 其他
class PlateDemo1<T> {
//盘子里的东西
private T someThing;
}
通过定义为类型Object的引用,来实现参数的“任意化”,结果如下
//盘子,可以装 任何东西,包括 食物 其他
class PlateDemo1 {
//盘子里的东西
private Object someThing;
}
Object实现参数的 “泛型化”、“任意化”带来的缺点是:要做显式的强制类型转换。
参数类型强制转换有一个大大降低代码复用性和扩展性的坏处:
而引入泛型后,有如下好处:
1、避免了强制类型转换,提高代码的复用性和扩展性
泛型中,所有的类型转换都是自动和隐式的,不需要强制类型转换,可以提高代码的重用率,再加上明确的类型信息,代码的可读性也会更好。
2、把运行时期的问题提前到了编译期,编译时的类型检查,使程序更加健壮
使用普通的Object泛化,对于强制类型转换错误的情况,编译期不会提示错误,在运行的时候才出现异常,这是一个安全隐患。
泛型的好处是在编译期检查类型安全,并能捕捉类型不匹配的错误,避免运行时抛出类型转化异常ClassCastException,将运行时错误提前到编译时错误,消除安全隐患。
正是由于以上两点原因,泛型得到了广泛的应用。
比如Java中,所有的标准集合接口都是泛型化的:Collection<V>
、List<V>
、Set<V>
和 Map<K,V>
。
现在我定义一个“水果盘子”,用来装苹果, 逻辑上水果盘子当然可以装苹果。
那么,一个“装苹果的盘子”,能转换成一个“装水果的盘子”吗?
看下面的例子
那么,一个“装苹果的盘子”,能转换成一个“装水果的盘子”吗? 答案是不行的。
编译器 的逻辑是这样的:
也就是说:就算 苹果 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)的作用,实现了 子类泛型对象 到 父类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) 作用,实现了 复类泛型对象 到 子类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 T>
来接收返回的数据,此写法的泛型集合不能使用 add 方法, 而<? super T>
不能使用 get 方法,两者在接口调用赋值的场景中容易出错。
Producer Extends 上界生产,就是 生产者使用 “? extends T”通配符。
以“? extends T”声明的集合,不能往此集合中添加元素,所以它也只能作为生产者,如下:
所以,使用 “? extends T” 上界,能轻松地成为 producer 生产者,完成
读取元素
迭代元素
这就是 Producer Extends 上界生产,就是 生产者使用 “? extends 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 ,在逻辑上看,可以看成是多个不同的类型,实际上都是相同类型
ArrayList<Food> list1
ArrayList<Fruit> list2
ArrayList<Apple> list3
.....
泛型类型在逻辑上可以看成是多个不同的类型,但实际上都是相同的类型。
看下面的例子
类型参数在运行中并不存在,这意味着:
Java泛型的实现是靠类型擦除技术实现的,类型擦除是在编译期完成的,泛型擦除怎么做呢?
当泛型类型被声明为一个具体的泛型标识,或一个无界通配符
时,泛型类型将会被替代为Object
。
这也比较容易理解,如 List<?>,PlateDemo1<?>
, 当获取元素的时,因为不能够确定具体的类型,所以只能使用Object
来接收,
在擦除的时候也是一样的道理,无法确定具体类型,所以擦除泛型时会将其替换为Object
类型,如:
当泛型类型被声明为一个上界通配符
时,泛型类型将会被替代为相应上界的类型。
主要,这里的上界,指的是用于类型定义场景里边的上界:
而不是变量定义场景里边用到到泛型上界,如下:
List<? extends Fruit> producer =...;
用泛型上界定义class的时候,指的是用于类型定义,泛型类型将会被替代为相应上界的类型。
下界通配符
的擦除,同无界通配符
,
下届只能定义引用的时候用,在定义类型的时候用不了,所以下界擦除只能替换为Object
。
下界擦除只能替换为Object
。
以上的内容,如果大家能对答如流,如数家珍,基本上 面试官会被你 震惊到、吸引到。
最终,让面试官爱到 “不能自已、口水直流”。 offer, 也就来了。
其实, “offer自由” 不难实现, 前段时间一个跟尼恩卷了2年的武汉小伙,9年经验, 在年底大裁员的极度严寒/痛苦被裁的背景下, offer拿到手软, 实现真正的 “offer自由” 。
在面试之前,建议大家系统化的刷一波 5000页《尼恩Java面试宝典PDF》,里边有大量的大厂真题、面试难题、架构难题。很多小伙伴刷完后, 吊打面试官, 大厂横着走。
在刷题过程中,如果有啥问题,大家可以来 找 40岁老架构师尼恩交流。
尼恩一直深耕技术,不是在研究技术,就是在研究技术的路上,加尼恩微信之后不一定立马通过, 但是,最多1-2小时就会审核的。
特别要说的是:很多小伙伴简历投出去后如泥牛入海、不冒一泡、没有面试机会。
遇到这种难题,可以找尼恩来改简历、做帮扶。
另外,遇到架构升级、晋升受阻、职业打击等职业难题,也可以找尼恩取经, 可以省去太多的折腾,省去太多的弯路。
尼恩已经指导了大量的小伙伴上岸,前段时间指导一个40岁+被裁小伙伴上岸,拿到了一个年薪100W的offer。
……完整版尼恩技术圣经PDF集群,请找尼恩领取
《尼恩 架构笔记》《尼恩高并发三部曲》《尼恩Java面试宝典》PDF,请到下面公号【技术自由圈】取↓↓↓