微信公众号:大黄奔跑
关注我,可了解更多有趣的面试相关问题。
写在之前
本文主要向面试角度讲述一个问题在面试中该如何回答,如果只想看问题答案,可以重点留意灰色背景的问题,其他的文字主要是扩展内容。
关于泛型,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
的程序员,咱们下期见。