面试中泛型问题一文搞定
关注我,可了解更多有趣的面试相关问题。
写在之前
本文主要从面试角度讲述泛型问题在面试中该如何回答,如果只想看问题答案,可以重点留意灰色背景的问题,其他的文字主要是扩展内容。
关于泛型,Jdk1.5
以后推出来的特性,一经推出就收到Java
程序员大力赞扬,因为真的解决一些编码问题。下面我们来看看如此重要的泛型,在面试中会怎么考察呢。
面试问题概览
下面我罗列一些大厂面试中,对于泛型一些常见问题。
- 泛型了解吗?介绍一下泛型?【快手】
- 泛型实现的原理,为什么要实现泛型【阿里】
- 泛型实现原理 【去哪儿】
java
的泛型,super
和extend
的区别?泛型擦除的原理【华为】- 什么是泛型,什么时候需要利用泛型【字节跳动】
java
的泛型,有什么缺点【腾讯】
可以看到泛型在各个大厂面试中已经成为了笔考题目,回想自己当初校招,因为泛型回答不好而错失阿里Offer,至今仍然悔恨不已。
真实面试回顾
一个身着灰色格子衬衫,拿着闪着硕大的🍎logo小哥迎面走来,我心想,logo还自带发光的,这尼玛肯定是p7大佬了,但是刚开始咱们还是得淡定不是。
大黄同学是吧,我看你简历上面写熟悉
Java
基础以及常见的Java
特性。那你先说说你对泛型的理解吧
此刻咱们得淡定,虽然自己准备过,也得慢慢道来。
面试官您好,泛型是JDK1.5之后提出的新概念,本意是让同一套代码可以适应更多的类型。
大家都写过议论文对吧,对于一个问题需要正反两面说,优点、缺点,一一道来。
缺点:在没有泛型之前一旦某个接口定义了参数为某个类型,则实现了该接口的方法必须采用同样类型参数,不利于程序的扩展。
优点:
- 比如在创建容器的时候,指定容器持有何种类型,以便在编译期间就能够保证类型的正确性,而不是将错误留到运行的时候。
- 无论是创建类、方法的时候都可以泛化参数,可以做到代码的复用。
泛型的出现最主要目的为了更好的创建容器类
比如下面这个例子中,在定义Dog
类的时候不需要指定其属性a的含义,只需要利用T
来表示,在new
该对象的时候指定对应的类型即可。
/** * * @param <T> */ public class Dog<T> { private T a; public Dog(T a) { this.a = a; } public void set(T a) { this.a = a; } public T get() { return a; } public static void main(String[] args) { // 在实例化的时候才确定类型 Dog<Integer> h3 = new Dog<Integer>(new Integer(3)); // 然后就可以不用强制类型转化了 Integer a = h3.get(); // h3.set("Not an Automobile"); // Error // h3.set(1.11); // Error } }
面试官摸了摸笔记本,心想,回答的还可以,继续追问。
你刚才说泛型消除了类型之间的差异,那你知道jdk底层是如何做到的吗?
大黄:
jdk是通过类型擦除来实现泛型的,编译器在编译之后会擦除了所有类型相关的信息,所以在运行时不存在任何类型相关的信息。比如如果编写
List<String> names = new Arraylist<>();
在运行的时候,仅用List来表示。
先不要高兴,面试官可能会继续追问。
面试官:既然你说编译时就已经擦除了,为什么要用类型擦除呢?
大黄:这么做是为了向前兼容,为了兼容jdk1.5以前的程序,确保能和Java 5之前的版本开发二进制类库进行兼容。
好小伙,这都知道,看来之前做足了功课,还得继续问问,试试深浅。
面试官:你知道泛型中的通配符吧,那你说一下什么是限定通配符和非限定通配符。
大黄:限定通配符,是限定类型必须是某一个类型的子类。
比如:List<? extends Fruit> flist
表示具有任何从Fruit继承的类型的列表。
而限定通配符:<? super Fruit> 确保类型必须是Fruit的父类来设定类型的下界。
大黄小提醒
不要意味new
一个List<? extends Fruit>
的flist
就可以把任何的水果塞到flist
里面,很遗憾的告诉你,下面的程序编译不通过的。
/** * 泛型中通配符的应用 */ public class GenericsAndCovariance { public static void main(String[] args) { // new一个水果篮子 List<? extends Fruit> flist = new ArrayList<Apple>(); // 下面编译不会通过 flist.add(new Apple()); // 下面编译不会通过 flist.add(new Orange()); // 下面编译同样不会通过 // flist.add(new Fruit()); } }
看到这个程序是否感觉到有点懵,我自己创建的水果篮子,我无法往里面放苹果、橙子,甚至连篮子对象也不能放。那我这个篮子还有屁用哦。
是的,确实没有什么用,似乎和预期不太一样。
但是想想也合乎道理。如果不知道list
持有什么对象,那么怎么样才能安全地向其中添加对象呢?如果允许了,则获取元素的时候,就会出错。
下面面试继续:
面试官:那你再说说泛型可以用到什么地方?
大黄:泛型即可用用于类定义中、接口的定义、方法的定义、同时也广泛用于容器中。
- 在类中使用泛型,可以让类中的属性由泛型定义,而不需要提前知道属性的类型。
- 在接口中使用泛型,可以让接口方法的返回值按照各自实现自由定义。这也是常见的工厂设计模式的一种应用。
- 在方法中利用泛型,可以做到方法的复用,同一个方法可以传入不同类型参数。实现时只需要将泛型参数列表置于返回值之前即可,JDK1.8中Stream中很多流式方法的定义采用了泛型。
记得加上这句话
我觉得,使用泛型机制最吸引人的地方,在使用容器的地方,比如List、Set、Map,因为在1.5以前,在泛型出现之前,当将一个对象放到容器中,这个对象会默认的转化为Object类型,因此会丢失掉类型信息,可能将一个Apple对象放到Orange容器中,然后试图从Orange容器中获取对象时,得到的是一个Apple,强制转化必然出问题,会抛出RuntimeException,但是有了泛型之后,这种问题在写代码,也就是说在编译期间就会暴露,而不是在运行时暴露。
比如下面定义接口用于生成不同的对象:
/** * 定义一个生成器 * @param <T> */ public interface Generator<T> { /** * 定义生成下一个对象 * @return */ T next(); } /** * 利用泛型生成器来生成费波列切数 */ public class Fibonacci implements Generator<Integer> { private int count = 0; public static void main(String[] args) { Fibonacci gen = new Fibonacci(); for (int i = 0; i < 18; i++) System.out.print(gen.next() + " "); } /** * 实现接口的方法,返回值为Integer * @return */ public Integer next() { return fib(count++); } /** * 定义费波列切数 * @param n * @return */ private int fib(int n) { if (n < 2) return 1; return fib(n - 2) + fib(n - 1); } }
泛型方法的应用:
public class GenericMethods { public static void main(String[] args) { GenericMethods gm = new GenericMethods(); gm.f(""); gm.f(1); gm.f(gm); } /** * 定义泛型方法 * @param x 方法的参数为泛型 * @param <T> */ public <T> void f(T x) { System.out.println(x.getClass().getName()); } }
回答到这里,关于泛型的问题基本上总结的差不多了。
总结
本身主要围绕开头的几个真正的面试题展开,简单来说,泛型是什么?为什么要有泛型?泛型如何实现的?泛型有哪些用处。
个人观点,一个技术从刚开始学习的时候,从四方面思考,能够事半功倍。
最后大黄分享多年面试心得。面试中,面对一个问题,大概按照总分的逻辑回答即可。先直接抛出结论,然后举例论证自己的结论。一定要第一时间抓住面试官的心里,否则容易给人抓不着重点或者不着边际的印象。
番外
另外,关注大黄奔跑,第一时间收获独家整理的面试实战记录及面试知识点总结。
我是大黄,一个只会写HelloWorld
的程序员,咱们下期见。