十道泛型面试题,你答得上来吗?

问题一:为什么需要泛型?

答:
使用泛型机制编写的代码要比那些杂乱的使用Object变量,然后再进行强制类型转换的代码具有更好的安全性和可读性,也就是说使用泛型机制编写的代码可以被很多不同类型的对象所重用。

问题二:从ArrayList的角度说一下为什么要用泛型?

答:
在Java增加泛型机制之前就已经有一个ArrayList类,这个ArrayList类的泛型概念是使用继承来实现的。

public class ArrayList {    
    private Object[] elementData;    
    public Object get(int i) {....}    
    public void add(Object o) {....}
}

这个类存在两个问题:

1.当获取一个值的时候必须进行强制类型转换
2.没有错误检查,可以向数组中添加任何类的对象

ArrayList files = new ArrayList();
files.add(new File(""));
String filename = (String)files.get(0);

对于这个调用,编译和运行都不会出错,但是当我们在其他地方使用get方法获取刚刚存入的这个File对象强转为String类型的时候就会产生一个错误。

泛型对于这种问题的解决方案是提供一个类型参数

ArrayList<String> files = new ArrayList<>();

这样可以使代码具有更好的可读性,我们一看就知道这个数据列表中包含的是String对象。 编译器也可以很好地利用这个信息,当我们调用get的时候,不需要再使用强制类型转换,编译器就知道返回值类型为String,而不是Object:

String filename = files.get(0);

编译器还知道ArrayList中add方法中有一个类型为String的参数。这将比使用Object类型的参数安全一些,现在编译器可以检查,避免插入错误类型的对象:

files.add(new File(""));

这样的代码是无法通过编译的,出现编译错误比类在运行时出现类的强制类型转换异常要好得多。

问题三:说说泛型类吧

一个泛型类就是具有一个或多个类型变量的类,对于这个类来说,我们只关注泛型,而不会为数据存储的细节烦恼。

public class Couple<T> {
   private T one;
   private T two;
}

Singer类引入了一个类型变量T,用尖括号括起来,并放在类名的后面。泛型类可以有多个类型变量:

public class Couple<T, U> {...}

类定义中的类型变量是指定方法的返回类型以及域和局部变量的类型

//域
private T one;
//返回类型
public T getOne() { return one;}
//局部变量
public void setOne(T newValue) { one = newValue; }

使用具体的类型代替类型变量就可以实例化泛型类型:

Couple<Rapper>

泛型类可以看成是普通类的工厂,打个比方:我用泛型造了一个模型,具体填充什么样的材质,由使用者去做决定。

问题四: 说说泛型方法的定义和使用

答:
泛型方法可以定义在普通类中,也可以定义在泛型类中,类型变量是放在修饰符的后面,返回类型的前面。
我们来看一个泛型方法的实例:

class ArrayUtil {

    public static <T> T getMiddle(T...a){
        return a[a.length / 2];
    }
}

当调用一个泛型方法时,在方法名前的尖括号中放入具体的类型:

String middle = ArrayUtil.<String>getMiddle("a","b","c");

在这种情况下,方法调用中可以省略类型参数,编译器会使用类型推断来推断出所调用的方法,也就是说可以这么写:

String middle = ArrayAlg.getMiddle("a","b","c");

问题五:E V T K ? 这些是什么

答:

E——Element 表示元素 特性是一种枚举T——Type 类,是指Java类型K—— Key 键V——Value 值?——在使用中表示不确定类型

问题六:了解过类型变量的限定吗?

答:
一个类型变量或通配符可以有多个限定,例如:

<T extends Serializable & Cloneable>

单个类型变量的多个限定类型使用&分隔,而,用来分隔多个类型变量。

<T extends Serializable,Cloneable>

在类型变量的继承中,可以根据需要拥有多个接口超类型,但是限定中至多有一个类。如果用一个类作为限定,它必定是限定列表中的第一个。
在这里插入图片描述
类型变量的限定是为了限制泛型的行为,指定了只有实现了特定接口的类才可以作为类型变量去实例化一个类。

问题七:泛型与继承你知道多少?

答:
首先,我们来看一个类和它的子类,比如 Singer 和 Rapper。但是Couple却并不是Couple的一个子类。

无论S和T有什么联系,Couple与Couple没有什么联系。

这里需要注意泛型和Java数组之间的区别,可以将一个Rapper[]数组赋给一个类型为Singer[]的变量:

Rapper[] rappers = ...;
Singer[] singer = rappers;

然而,数组带有特别的保护,如果试图将一个超类存储到一个子类数组中,虚拟机会抛出ArrayStoreException异常。

问题八:聊聊通配符吧

答:

通配符类型中,允许类型参数变化。比如,通配符类型:

Couple<? extends Singer>

表示任何泛型类型,它的类型参数是Singer的子类,如Couple,但不会是Couple。

假如现在我们需要编写一个方法去打印一些东西:

public static void printCps(Couple<Rapper> cps) {
      Rapper one = cp.getOne();      
      Rapper two = cp.getTwo();      
      System.out.println(one.getName() + " & " + two.getName() + " are cps.");
}

正如前面所讲到的,不能将Couple传递给这个方法,这一点很受限制。解决的方案很简单,使用通配符类型:

public static void printCps(Couple< ? extends Singer> cps) 

Couple是Couple< ? extends Singer>的子类型。

我们接下来来考虑另外一个问题,使用通配符会通过Couple< ? extends Singer>的引用破坏Couple吗?

Couple<Rapper> rapper = new Couple<>(rapper1, rapper2);
Couple<? extends Singer> singer = rapper;
player.setOne(reader);

这样可能会引起破坏,但是当我们调用setOne的时候,如果调用的不是Singer的子类Rapper类的对象,而是其他Singer子类的对象,就会出错。 我们来看一下Couple<? extends Singer>的方法:

? extends Singer getOne();
void setOne(? extends Singer);

这样就会看的很明显,因为如果我们去调用setOne()方法,编译器之可以知道是某个Singer的子类型,而不能确定具体是什么类型,它拒绝传递任何特定的类型,因为 ? 不能用来匹配。但是使用getOne就不存在这个问题,因为我们无需care它获取到的类型是什么,但一定是Singer的子类。

通配符限定与类型变量限定非常相似,但是通配符类型还有一个附加的能力,即可以指定一个超类型限定:

? super Rapper

这个通配符限制为Rapper的所有父类,为什么要这么做呢?带有超类型限定的通配符的行为与子类型限定的通配符行为完全相反,可以为方法提供参数,但是却不能获取具体的值,即访问器是不安全的,而更改器方法是安全的:

编译器无法知道setOne方法的具体类型,因此调用这个方法时不能接收类型为Singer或Object的参数。只能传递Rapper类型的对象,或者某个子类型(Reader)对象。而且,如果调用getOne,不能保证返回对象的类型。

总结一下:

带有超类型限定的通配符可以向泛型对象写入,带有子类型限定的通配符可以从泛型对象读取。

问题九:泛型在虚拟机中是什么样呢?

答:

1.**虚拟机没有泛型类型对象,所有的对象都属于普通类。**无论何时定义一个泛型类型,都自动提供了一个相应的原始类型。原始类型的名字就是删去类型参数后的泛型类型名。擦除类型变量,并替换成限定类型(没有限定的变量用Object)。这样做的目的是为了让非泛型的Java程序在后续支持泛型的 jvm 上还可以运行(向后兼容)

2.当程序调用泛型方法时,如果擦除返回类型,编译器插入强制类型转换。

Couple<Singer> cps = ...;
Singer one = cp.getOne();

擦除cp.getOne的返回类型后将返回Object类型。编译器自动插入Singer的强制类型转换。也就是说,编译器把这个方法调用编译为两条虚拟机指令:

对原始方法cp.getOne的调用 将返回的Object类型强制转换为Singer类型。

3.当存取一个公有泛型域时也要插入强制类型转换。

//我们写的代码
Singer one = cps.one;
//编译器做的事情
Singer one = (Singer)cps.one;

问题十:关于泛型擦除,你知道多少?

答:

类型擦除会出现在泛型方法中,程序员通常认为下述的泛型方法

public static <T extends Comparable> T min(T[] a)

是一个完整的方法族,而擦除类型之后,只剩下一个方法:

public static Comparable min(Comparable[] a)

这个时候类型参数T已经被擦除了,只留下了限定类型Comparable。

但是方法的擦除会带来一些问题:

class Coupling extends Couple<People> {
    public void setTwo(People people) {
            super.setTwo(people);
    }
}

擦除后:

class Coupling extends Couple {
    public void setTwo(People People) {...}
}

这时,问题出现了,存在另一个从Couple类继承的setTwo方法,即:

public void setTwo(Object two)

这显然是一个不同的方法,因为它有一个不同类型的参数(Object),而不是People。

Coupling coupling = new Coupling(...);
Couple<People> cp = interval;
cp.setTwo(people);

这里,希望对setTwo的调用具有多态性,并调用最合适的那个方法。由于cp引用Coupling对象,所以应该调用Coupling.setTwo。问题在于类型擦除与多态发生了冲突。要解决这个问题,就需要编译器在Coupling类中生成一个桥方法:

public void setTwo(Object second) {
    setTwo((People)second);
}

变量cp已经声明为类型Couple,并且这个类型只有一个简单的方法叫setTwo,即setTwo(Object)。虚拟机用cp引用的对象调用这个方法。这个对象是Coupling类型的,所以会调用Coupling.setTwo(Object)方法。这个方法是合成的桥方法。它会调用Coupling.setTwo(Date),这也正是我们所期望的结果。

所以,我们要记住关于Java泛型转换的几个点:

1.虚拟机中没有泛型,只有普通的类和方法;
2.所有的类型参数都用它们的限定类型替换;
3.桥方法被合成来保持多态;
4.为保持类型安全性,必要时插入强制类型转换。

总结

除了上面提到的泛型面试题之外,我还整理了许多Java架构进阶资料以及BATJ等大厂面试题资料,内容如下截图
Java架构进阶资料
关于本篇文章如果有任何错误、疑问或者补充欢迎大家留言或私信我。如果对我写的文章感兴趣,或者想获取架构进阶以及大厂面试资料的可以加入我的Java学习交流社区,相互交流学习!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值