目录
第一问:什么是泛型?有什么好处?
第二问:泛型是如何实现的呢?
第三问:类型擦除的缺点有哪些?
第四问:泛型中上下界限定符extends和super有什么区别?
第五问:List、List、List之间的区别?
第六问:如何在泛型为Integer的ArrayList中存放一个String类型的对象?
我们用问面试题的方式来讲解集合的泛型概念,这样方便读者理解。
第一问:什么是泛型?有什么好处?
Java泛型(generics) 是JDK5中引入的一个新特性,允许在定义类和接口的时候使用类型参数(type parameter)。声明的类型参数在使用时用具体的类型来替换。泛型最主要的应用是在JDK5中的新集合类框架中。
泛型的好处有两个:
- 方便:可以提高代码的复用性。以List接口为例,我们可以将String、Integer等类型放入List中,如不用泛型,存放String类型要写一个List接口,存放Integer要写另外一个List接口,泛型可以很好的解决这个问题。
- 安全:在泛型出现之前,通过Object实现的类型转换需要在运行时检查,如果类型转换出错,程序直接GG,可能会带来毁灭性打击。而泛型的作用就是在编译时做类型检查,这无疑增加程序的安全性。
第二问:泛型是如何实现的呢?
Java中的泛型通过类型擦除的方式来实现,通俗点理解,就是通过语法糖的形式,在java->.class转换的阶段,将List<String>擦除调转为List的手段。换句话说,Java的泛型只在编译期,jvm是不会感知到泛型的。
比如Java的编译器在编译以下代码时:
public class Foo<T> {T bar;void doSth(T param) {}
};Foo<String> f1;
Foo<Integer> f2;
在编译后的字节码文件中,会把泛型的信息擦除掉:
public class Foo {Object bar;void doSth(Object param) {}
};
也就是说,在代码中的Foo<String> 和 Foo<Integer>使用的类,经过编译后都是同一个类。
所以说泛型技术实际上是Java语言的一颗语法糖,因为泛型经过编译器处理之后就被擦除了。
这种擦除的过程,被称之为——类型擦除。所以类型擦除指的是通过类型参数名T,将泛型类型实例关联到同一份字节码上。编译器只为泛型类型生成一份字节码,并将其实例关联到这份字节码上。类型擦除的关键在于从泛型类型中清除类型变量的相关信息,并在仅需要的时候插入类型标记和类型转换的方法。
第三问:类型擦除的缺点有哪些?
- 泛型不可以重载
- 泛型异常类不可以多次catch
- 泛型类中的静态变量也只有一份,不会有多份
第四问:泛型中上下界限定符extends和super有什么区别?
<? extends T> 表示类型的上界,表示参数化类型可能是T或是T的子类。
// 定义一个泛型方法,接受任何继承自Number的类型
public <T extends Number> void processNumber(T number) {
// 在这个方法中,可以安全地调用Number的方法
double value = number.doubleValue();
// 其他操作...
}
<? super T> 表示类型下界(Java Core中叫超类型限定),表示参数化类型是此类型的超类型(父类型),直至Object。
// 定义一个泛型方法,接受任何类型的List,并向其中添加元素
public <T> void addElements(List<? super T> list, T element) {
list.add(element);
// 其他操作...
}
在使用限定通配符的时候,需要遵守PECS原则,即Producer Extends, Consumer Super:上界生产,下界消费。
如果是从集合中读取类型T的数据,并且不能写入,可以使用<? extends 通配符; (Producer Extends),如上面的processNumber方法。
如果是从集合中写入类型T的数据,并且不需要读取,可以使用<? super 通配符; (Consumer Super),如上面的addElements方法。
如果既要存又要取,那就不要用任何通配符。
第五问:List<?>、List<Object>、List之间的区别?
-
List<?> 是一个未知类型的List,而List<Object> 其实是任意类型的List。可以把List<String>、List<Integer>赋值给List<?>,却不能把List<String>赋值给List<Object>。
-
可以把任何带参数的类型传递给原始类型List,但却不能把List<String>赋值给List<Object>,因为会产生编译错误(不支持协变)。
-
List<?>由于不确定列表中元素的具体类型,因此只能从这种列表中读取数据,而不能往里面添加除了null之外的任何元素。
public class Test {public static void main(String[] args) {List<String> list = new ArrayList<>();test(list);// 编译出错test1(list);test2(list);test3(list);}public static void test(List list) {list.add("CLAY");list.add(666);}public static void test1(List<Object> list) {list.add(new Test());}public static void test2(List<?> list) {// 编译出错list.add("CLAY");// 编译正常list.get(0);}public static void test3(List<String> list) {list.add("CLAY");}
}
第六问:如何在泛型为Integer的ArrayList中存放一个String类型的对象?
通过反射可以实现:
List<Integer> list = new ArrayList<>();
Method method = list.getClass().getMethod("add", Object.class);
method.invoke(list, "Java反射机制实例");
System.out.println(list.get(0));