泛型面试题大全
说一下你对泛型的理解
泛型可以实现,在使用时才具体指定参数的类型,它可以使用在类、接口、方法中。
泛型和Object相比,有什么优势?
使用 Object 有两个痛点:1、得强制转换;2、没有编译器级别的安全校验;
使用泛型后,不需要强制转换,如果我们指定了类型之后,如果设置的类型不正确,会报错。这是泛型的优势。
介绍一下泛型擦除
Java的泛型其实是伪泛型,仅在语法上支持泛型,在编译的阶段,会进行泛型擦除。简单来说,就是编译时,如果识别到泛型的语法(<>尖括号及其内容),就将其替换成具体的类型。
替换的规则是这样的,如果有设置上下边界,就按上下边界设置具体类型,没有就设置成Object。
泛型为什么不支持基本类型
因为泛型在没有定义边界的时,进行类型擦除后,原始类型会变成Object,而基本类型不是对象,更不会继承于Object,没有多态性(说白了就是不能隐式转成使用时的目标类型),这就是不支持基本类型的根本原因。
说一下泛型的桥接方法(低概率会问到)
桥接方法只出现在子类重写了父类的泛型方法后。
因为父类的泛型方法,在泛型擦除后,会变成一个具体的类型(比如Object)。而子类覆盖父类这个泛型方法后,可能会指定一个具体类型(比如String)。这会导致父类原本 Object 的那个泛型方法没有被实现,这会让父类没了多态性。
具体的影响,就是当我们使用父类API进行统一调用的时候,可能会出现运行时错误。因为入参或者返回的数据,和我们写代码时候预期的不一样,就会出现类型转换的错误。
Java给出的解决方案是,在子类重写了父类的泛型方法后,编译器会自动生成对应的桥接方法。桥接方法的名称、入参、返回值和父类一致,方法内容就是去调用我们重写的方法。
知识点
在JDK5的时候,Java引入了泛型这个特性。
泛型让我们可以实现,在使用时才具体指定参数的类型。
泛型可以使用在类、接口、方法中。
泛型的意义
其实使用 Object ,也可以实现泛型的所有工作。
那为什么还要引入泛型呢?
因为Object 有两个痛点:1、得强制转换。2、没有编译器级别的安全校验。
例子如下:
没有引入泛型前,Java都是使用 Object 来实现类似泛型功能的
在实际开发中,我们经常会定义一个通用的返回结果类,如下:
传统使用 Object
public class CommonResult{
public boolean success = true;
public Object data;
}
使用泛型
public class CommonResult<T>{
public boolean success = true;
public T data;
}
消除转换
使用传统的 Object 的 CommonResult
如下例子可以看到,当我们需要取出 result.data 来使用的时候,是需要进行强制类型转换的。
public static void main(String[] args){
CommonResult result = new CommonResult();
// 1、给data赋值,
result.data = "呵呵哒";
// 2、把 result.data的值放到 data 变量里
String data = (String) result.data;
}
使用泛型的 CommonResult
我们给 CommonResult 指定类型,后续使用的时候,无需强制转换。
相对传统 Object 来说,会比较优雅一些。
public static void main(String[] args){
CommonResult<String> result = new CommonResult();
// 1、给data赋值,
result.data = "呵呵哒";
// 2、把 result.data的值放到 data 变量里
String data = result.data;
}
安全性
使用传统的 Object 的 CommonResult
由于使用时需要进行类型的强制转换,但Object里面可能装载着任意类型,并非强制转换的目标类型,就有可能发生运行时异常。
第一步:给 result.data 赋值为字符串的“1”
第二步:由于第一步赋值的是字符串的“1”,强制转换成 int 会报一个类型转换的异常。
public static void main(String[] args){
CommonResult result = new CommonResult();
// 1、给data赋值,
result.data = "1";
// 2、把 result.data的值放到 data 变量里
int data = (int) result.data;
}
使用泛型的 CommonResult 就可以避免这个问题
首先,我们给 CommonResult 指定类型
第一步:我们给 result.data 赋值为字符串的“1”,此时编译器会报错,因为赋的值的不是指定的类型。
public static void main(String[] args){
CommonResult<Integer> result = new CommonResult();
// 1、给data赋值,
result.data = "1";
// 2、把 result.data的值放到 data 变量里
Integer data = result.data;
}
泛型标记符
- E - Element (在集合中使用,因为集合中存放的是元素)
- T - Type(Java 类)
- K - Key(键)
- V - Value(值)
- N - Number(数值类型)
- ? - 表示不确定的 java 类型
上下边界设定
定义上边界,以下实例代码标识,泛型的类型只能是 A 或者 A的子孙类
public class Test<T extends A>{}
定义下边界,以下实例代码表示,泛型的类型只能是 A 或者 A的超类
public class Test<T super A>{}
不设定边界 ,在不是E,K 这类业务的情况下,使用 T、? 都行
public class Test<T>{} public class Test<?>{}
泛型定义
泛型类
public class CommonResult<T> {}
泛型方法
public class CommonResult {
// 入参是泛型
public <T> void setData(T data){}
// 返回值是泛型
public <T> T setData(Object data){}
// 入参和返回值都是泛型
public <T> T setData(T data){}
}
泛型的原理
Java的泛型,实际上是使用伪泛型的策略,仅在Java的语法上支持泛型,在编译的阶段会进行 泛型擦除 。简单来说,就是在编译时,如果识别到泛型的语法(<>尖括号及其内容),就将其替换成具体的类型。
泛型擦除的原则
- 泛型擦除发生在编译期间;
- 当识别到类型参数声明,就删除<>包围及其内容;
- 如果设置了上下边界,则按设定的上下边界进行替换。如果没有设定上下边界,则替换为Object;
- 自动产生“桥接方法”以保证擦除类型后的代码仍然具有泛型的“多态性”;
具体是怎么擦除的?
无边界的情况
还是以上面的 CommonResult 为例
public class CommonResult<T>{
public boolean success = true;
public T data;
}
擦除后
public class CommonResult{
public boolean success = true;
public Object data;
}
有边界的情况
public class CommonResult<T extends A>{
public boolean success = true;
public T data;
}
擦除后
public class CommonResult{
public boolean success = true;
public A data;
}
泛型的桥接方法
当子类重写了父类的泛型方法后,编译器会在该子类中自动生成桥接方法。
以 CommonResult 举一个例子
public class CommonResult<T>{
private T data;
public T getData() {
return data;
}
public void setData(T data) {
this.data = data;
}
}
假设有个需求,要在CommonResult的基础上,写一个只能返回和设置 String 类型的子类 StringResult ,如下
public class StringResult extends CommonResult<String>{
@Override
public String getData() {
return data;
}
@Override
public void setData(String data) {
this.data = data;
}
}
继承时指定了类型为CommonResult ,并且重写了 getData 和 setData方法,让其只能返回和接受 String 类型。看起来没啥问题哈,重写是可以的,编译器也没报错。
可是,从前面说的【具体是怎么擦除的? 】案例来看,父类 CommonResult 在进行类型擦除后,会变成Object,如下:
public class CommonResult<T>{
private Object data;
public Object getData() {
return data;
}
public void setData(Object data) {
this.data = data;
}
}
由于父类的 返回值 和 入参 在类型擦除后,是 Object 。
而 StringResult 重写了父类的 getData()
setData()
方法,并且把 返回值 和 入参 改成了 String ,导致父类原本的 返回值 和 入参 等于Object 的方法没有被实现。
当我们在使用父类的方法进行统一调用的时候,会出错的。
举个例子:
public static void main(String[] args) {
// 当变量类型是父类CommonResult,而实际指向的对象是子类StringResult时
CommonResult result = new StringResult();
// 由于 StringResult 重写 getData() 后,返回的是 String 对象,编译时没问题,运行时这行代码会出错
Object data = result.getData();
}
为了解决这个问题,Java采用了桥接方法的方案,具体的实现,看下面例子:
首先使用 javap -c StringResult.class
命令查看编译后的类信息,如下:
Compiled from "StringResult.java"
public class StringResult extends CommonResultN<java.lang.String> {
public StringResult();
Code:
0: aload_0
1: invokespecial #1 // Method com/mianshiking/common/model/CommonResultN."<init>":()V
4: return
public java.lang.String getData();
Code:
0: aload_0
1: invokespecial #2 // Method com/mianshiking/common/model/CommonResultN.getData:()Ljava/lang/Object;
4: checkcast #3 // class java/lang/String
7: areturn
public void setData(java.lang.String);
Code:
0: aload_0
1: aload_1
2: invokespecial #4 // Method com/mianshiking/common/model/CommonResultN.setData:(Ljava/lang/Object;)V
5: return
public void setData(java.lang.Object);
Code:
0: aload_0
1: aload_1
2: checkcast #3 // class java/lang/String
5: invokevirtual #5 // Method setData:(Ljava/lang/String;)V
8: return
public java.lang.Object getData();
Code:
0: aload_0
1: invokevirtual #6 // Method getData:()Ljava/lang/String;
4: areturn
}
可以看到,编译过后,编译器会自动生成对应的桥接方法,由桥接方法调用我们重写的方法。
有了桥接方法后,以上问题就解决了。
桥接方法对于开发人员来说是不感知的。
但是看编译后的这些代码,感觉有些奇奇怪怪,我把这些代码翻译成Java代码:
果然,编辑器报错了。
java: 已在类 com..StringResult中定义了方法 getData()
java: 名称冲突: com..StringResult 中的 setData(java.lang.String) 覆盖的方法的疑符与另一个方法的相同, 但两者均不覆盖对方
第一个方法: com..StringResult 中的 setData(java.lang.Object)
第二个方法: com..CommonResultN 中的 setData(T)
因为这些桥接方法,既不符合重载的要求,又不符合重写的语法标准。
对于编译器来说,它无法识别方法名和入参一样,但返回数据不一样的多个方法,所以编译器会报错。
但是对于JVM来说,它是根据返回参数类型 + 方法名 + 入参个数和类型来判断方法唯一性的。
所以Java为了实现自动桥接,允许了这个在编译器看起来不合法的事情,然后交给JVM去进行区别。
泛型为什么不支持基本类型
因为泛型在没有定义边界的时,进行类型擦除后,原始类型会变成Object,而基本类型不是对象,更不会继承于Object,没有多态性(说白了就是不能隐式转成使用时的目标类型),这就是不支持基本类型的根本原因。
public static void main(String[] args) {
// 这样会报错
List<int> list1 = new ArrayList();
List list2 = new ArrayList();
// 这里 add int 却可以 ,是因为Java提供自动装箱的能力,会自动把 int 装箱成 Integer
list1.add(1);
}