面试官:十问泛型,你能扛住吗?

4,075 阅读10分钟

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

答:

使用泛型机制编写的代码要比那些杂乱的使用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<String>add方法中有一个类型为String的参数。这将比使用Object类型的参数安全一些,现在编译器可以检查,避免插入错误类型的对象:

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

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

问题三:说说泛型类吧

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

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

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

public class Couple<TU{...}

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

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

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

Couple<Rapper>

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

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

答:

泛型方法可以定义在普通类中,也可以定义在泛型类中,类型变量是放在修饰符的后面返回类型的前面

我们来看一个泛型方法的实例:

class ArrayUtil {

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

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

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

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

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>

在类型变量的继承中,可以根据需要拥有多个接口超类型,但是限定中至多有一个类。如果用一个类作为限定,它必定是限定列表中的第一个

类型变量的限定是为了限制泛型的行为,指定了只有实现了特定接口的类才可以作为类型变量去实例化一个类。

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

答:

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

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

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

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

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

问题八:聊聊通配符吧

答:

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

Couple<? extends Singer>

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

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

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<Rapper>传递给这个方法,这一点很受限制。解决的方案很简单,使用通配符类型:

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

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

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

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方法的具体类型,因此调用这个方法时不能接收类型为SingerObject的参数。只能传递Rapper类型的对象,或者某个子类型(Reader)对象。而且,如果调用getOne,不能保证返回对象的类型。

总结一下:

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

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

答:

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

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

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

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

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

  1. 当存取一个公有泛型域时也要插入强制类型转换。
//我们写的代码
Singer one = cps.one;
//编译器做的事情
Singer one = (Singer)cps.one;

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

答:

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

public static <T extends Comparable> 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<LocalDate>,并且这个类型只有一个简单的方法叫setTwo,即setTwo(Object)。虚拟机用cp引用的对象调用这个方法。这个对象是Coupling类型的,所以会调用Coupling.setTwo(Object)方法。这个方法是合成的桥方法。它会调用Coupling.setTwo(Date),这也正是我们所期望的结果。

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

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

如果你有学到,请给我点赞👍+关注,这是对一个✊坚持原创作者的最大支持!我是山禾,千篇一律的皮囊,万里挑一的灵魂,一个不太一样的写手。