一次平平无奇的面试
为什么要写这篇文档,主要就是在字节二面的时候,面试官提了这么一个问题
面试官:Java中的List<Integer>里有可能存String类型元素吗?
当时的我:应该…不可以吧,好像编译器会报错!
面试官:你可以回去试一下,然后这题就嘎了
面试结束了以后,手撕了代码,结果发现通过反射的方式去操作是可行的!
public static void main(String[] args) throws NoSuchMethodException, SecurityException, IllegalAccessException, IllegalArgumentException, InvocationTargetException {
List<Integer> list = new ArrayList<Integer>();
list.add(1);
list.getClass().getMethod("add", Object.class).invoke(list, true);
System.out.println(list.toString()); //打印 [1, JAVA]
}
这里面试官主要想考察的是Java基础中的泛型,想知道你对泛型了解多少,包括泛型擦除,泛型限制等
深入泛型擦除
为什么要执行泛型擦除,什么是泛型擦除
1.Java虚拟机(JVM)并不了解泛型类型,它只认识普通类型;
2.早期Java并没有泛型的概念,为了兼容早期版本。
所以,泛型擦除是指在编译时将泛型类型参数擦除为其上界或下界类型的过程,实现泛型代码的向后兼容性。
具体来说,当Java编译器遇到泛型类型时,它会将泛型类型擦除为其上界类型。例如,对于一个泛型类List<T>,编译器会将它的擦除类型设置为List<Object>。这样做可以保证泛型类型在编译时的可用性,同时也避免了编译错误。
根据上面的代码,Java编译器的泛型擦除机制,会在编译期的时候,将List<Integer> list 擦除为 List<Object> list,这样做可以减少生成的字节码文件的大小,提高程序的运行效率。
ArrayList源码解析
public class ArrayList<E> extends AbstractList<E>
implements List<E>, RandomAccess, Cloneable, java.io.Serializable
{
...
private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};
transient Object[] elementData;
public ArrayList() {
this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
}
@SuppressWarnings("unchecked")
E elementData(int index) {
return (E) elementData[index];
}
public E get(int index) {
rangeCheck(index);
return elementData(index);
}
...
}
从源码可以看出,在我们初始化ArrayList的时候传入的类型,最终都会擦除成Object
在调用 get(int index) 时,返回数据通过强转,最后变成我们最开始传入的类型返回给调用方
常见泛型面试题
1.泛型的好处是什么?
答:泛型是Java 5中引入的特性,它提供了编译时类型安全检测机制,该机制允许程序员在编译时检测到非法的类型。泛型的本质是参数化类型,即给类型指定一个参数,然后在使用时再指定此参数具体的值,那样这个类型就可以在使用时决定了。这种参数类型可以用在类、接口和方法中,分别被称为泛型类、泛型接口、泛型方法。泛型的好处包括:
将运行时期的问题提前到编译时期解决,避免了强制类型转换。
增加代码的可读性和可维护性。
2.如何编写一个泛型方法,让它能接受泛型参数并返回泛型类型?
答:在Java中,可以使用或等通配符来表示泛型参数。
// 在这个示例中,<T>表示一个泛型参数,可以接受任何类型的参数。return语句返回一个泛型类型T。
public static <T> T min(T a, T b) {
if (a < b) {
return a;
} else {
return b;
}
}
3.如何使用泛型类来定义一个栈(Stack)?
// 在这个示例中,我们定义了一个名为Stack的类,并使用<T>来表示一个泛型参数。在类中,我们使用了一个List来保存栈中的元素,
// 并在push、pop、peek和isEmpty方法中分别对栈进行操作。由于使用了泛型类型,这个栈可以存储任何类型的元素。
public class Stack<T> {
private List<T> list = new ArrayList<>();
public void push(T item) {
list.add(item);
}
public T pop() {
return list.remove(list.size() - 1);
}
public T peek() {
return list.get(list.size() - 1);
}
public boolean isEmpty() {
return list.isEmpty();
}
}
4.什么是Java泛型的上界和下界?
Java泛型的上界和下界是指在声明泛型类型时,可以设置的上界和下界类型。例如List<? extends Number>表示该列表中只能存储Number类型的元素或者其子类,而List<? super Integer>表示该列表中可以存储Integer类型的元素或者其父类。
5.泛型中的E V T K 是什么?
E、V、T、K分别是Java泛型中常用的标记符,分别表示Element、Value、Type和Key。这些标记符可以用来限定泛型的类型,提高泛型的灵活性和类型安全性。
其中,E表示Element,即集合中的元素类型;V表示Value,即值类型;T表示Type,即类类型;K表示Key,即键类型。这些标记符可以用来限定泛型的类型,提高泛型的灵活性和类型安全性。例如,在Java中,可以使用Map<K, V>来创建一个键值对集合,其中K和V分别表示键和值的类型。