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

问题一:为何须要泛型?

答:java

使用泛型机制编写的代码要比那些杂乱的使用Object变量,而后再进行强制类型转换的代码具备更好的安全性和可读性,也就是说使用泛型机制编写的代码能够被不少不一样类型的对象所重用。程序员

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

答:web

在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类型的时候就会产生一个错误。app

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

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

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

String filename = files.get(0);

编译器还知道ArrayList<String>add方法中有一个类型为String的参数。这将比使用Object类型的参数安全一些,如今编译器能够检查,避免插入错误类型的对象:spa

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

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

问题三:说说泛型类吧

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

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>类型参数,编译器会使用类型推断来推断出所调用的方法,也就是说能够这么写:

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> 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<LocalDate>,而且这个类型只有一个简单的方法叫setTwo,即setTwo(Object)。虚拟机用cp引用的对象调用这个方法。这个对象是Coupling类型的,因此会调用Coupling.setTwo(Object)方法。这个方法是合成的桥方法。它会调用Coupling.setTwo(Date),这也正是咱们所指望的结果。

因此,咱们要记住关于Java泛型转换的几个点:

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