Java泛型面试题

1.Java中的泛型是什么 ? 使用泛型的好处是什么?以及各个版本有何区别?

答:泛型是 Java SE 1.5 的新特性,泛型的本质是参数化类型,这种参数类型可以用在类、接口和方法的创建中,分别称为泛型类、泛型接口、泛型方法。
在 Java SE 1.5 之前没有泛型的情况的下只能通过对类型 Object 的引用来实现参数的任意化,其带来的缺点是要做显式强制类型转换,而这种强制转换编译期是不做检查的,容易把问题留到运行时,所以 泛型的好处是在编译时检查类型安全,并且所有的强制转换都是自动和隐式的,提高了代码的重用率,避免在运行时出现 ClassCastException。

JDK 1.5 引入了泛型来允许强类型在编译时进行类型检查;JDK 1.7 泛型实例化类型具备了自动推断能力,譬如 List list = new ArrayList(); 可以写成 List list = new ArrayList<>(); 了,JDK 具备自动推断能力。下面几种写法可以说是不同版本的兼容性了:


image.png

2. Java的泛型是如何工作的 ? 什么是类型擦除 ?

答: 泛型是通过类型擦除来实现的,编译器在编译时擦除了所有泛型类型相关的信息,所以在运行时不存在任何泛型类型相关的信息,譬如 List 在运行时仅用一个 List 来表示,这样做的目的是为了和 Java 1.5 之前版本进行兼容。
泛型擦除具体来说就是在编译成字节码时首先进行类型检查,接着进行类型擦除(即所有类型参数都用他们的限定类型替换,包括类、变量和方法),接着如果类型擦除和多态性发生冲突时就在子类中生成桥方法解决,接着如果调用泛型方法的返回类型被擦除则在调用该方法时插入强制类型转换。

问:****Java 泛型类、泛型接口、泛型方法有什么区别?

答: 泛型类是在实例化类的对象时才能确定的类型,其定义譬如 class Test {},在实例化该类时必须指明泛型 T 的具体类型。

泛型接口与泛型类一样,其定义譬如 interface Generator { E dunc(E e); }。

泛型方法所在的类可以是泛型类也可以是非泛型类,是否拥有泛型方法与所在的类无关,所以在我们应用中应该尽可能使用泛型方法,不要放大作用空间,尤其是在 static 方法时 static 方法无法访问泛型类的类型参数,所以更应该使用泛型的 static 方法(声明泛型一定要写在 static 后返回值类型前)。泛型方法的定义譬如 void func(T val) {}。

问:****Java 如何优雅的实现元组?

答: 元组其实是关系数据库中的一个学术名词,一条记录就是一个元组,一个表就是一个关系,纪录组成表,元组生成关系,这就是关系数据库的核心理念。很多语言天生支持元组,譬如 Python 等,在语法本身支持元组的语言中元组是用括号表示的,如 (int, bool, string) 就是一个三元组类型,不过在 Java、C 等语言中就比较坑爹,语言语法本身不具备这个特性,所以在 Java 中我们如果想优雅实现元组就可以借助泛型类实现,如下是一个三元组类型的实现:

image

问:****下面程序块的运行结果是什么,为什么?

image

答: 上面代码段结果为 true,解释如下。

因为 load 的是同一个 class 文件,存在 ArrayList.class 文件但是不存在 ArrayList.class 文件,即便是通过 class.getTypeParameters() 方法获取类型信息也只能获取到 [T] 一样的泛型参数占位符。泛型是通过擦除来实现的,所以编译后任何具体的泛型类型都被擦除了(替换为非泛型上边界,如果没有指定边界则为 Object 类型),泛型类型只有在静态类型检查期间才出现,上面都被擦除成了 ArrayList 类型,所以运行时加载的是同一个 class 文件。

问:****为什么 Java 泛型要通过擦除来实现?****擦除有什么坏处或者说代价?

答: 可以说 Java 泛型的存在就是一个不得已的妥协,正因为这种妥协导致了 Java 泛型的混乱,甚至说是 JDK 泛型设计的失败。Java 之所以要通过擦除来实现泛型机制其实是为了兼容性考虑,只有这样才能让非泛化代码到泛化代码的转变过程建立在不破坏现有类库的实现上。正是因为这种兼容也带来了一些代价,譬如泛型不能显式地引用运行时类型的操作之中(如向上向下转型、instanceof 操作等),因为所有关于参数的信息都丢失了,所以任何时候使用泛型都要提醒自己背后的真实擦除类型到底是什么;此外擦除和兼容性导致了使用泛型并不是强制的(如 List list = new ArrayList(); 等写法);其次擦除会导致我们在编写代码时十分谨慎(如不想被擦除为 Object 类型时不要忘了添加上边界操作等)

问:****下面三个 funcX 方法有问题吗,为什么?

image

答:func1、func2、func3 三个方法均无法编译通过。

因为泛型擦除丢失了在泛型代码中执行某些操作的能力,任何在运行时需要知道确切类型信息的操作都将无法工作。

问:****下面代码段有问题吗,运行效果是什么,为什么?

image

答: 由于在程序中定义的 ArrayList 泛型类型实例化为 Integer 的对象,如果直接调用 add 方法则只能存储整形数据,不过当我们利用反射调用 add 方法时就可以存储字符串,因为 Integer 泛型实例在编译之后被擦除了,只保留了原始类型 Object,所以自然可以插入。

问:****请比较深入的谈谈你对 Java 泛型擦除的理解和带来的问题认识?

答:Java 的泛型是伪泛型,因为在编译期间所有的泛型信息都会被擦除掉,譬如 List 在运行时仅用一个 List 来表示(所以我们可以通过反射 add 方法来向 Integer 的泛型列表添加字符串,因为编译后都成了 Object),这样做的目的是为了和 Java 1.5 之前版本进行兼容。泛型擦除具体来说就是在编译成字节码时首先进行类型检查,接着进行类型擦除(即所有类型参数都用他们的限定类型替换,包括类、变量和方法,如果类型变量有限定则原始类型就用第一个边界的类型来替换,譬如 class Prd {} 的原始类型就是 Comparable),接着如果类型擦除和多态性发生冲突时就在子类中生成桥方法解决,接着如果调用泛型方法的返回类型被擦除则在调用该方法时插入强制类型转换。

先检查再擦除的类型检查是针对引用的,用引用调用泛型方法就会对这个引用调用的方法进行类型检测而无关它真正引用的对象。可以说这是为了兼容带来的问题,如下:

image

所以说擦除前的类型检查是针对引用的,用这个引用调用泛型方法就会对这个引用调用的方法进行类型检测而无关它真正引用的对象。

先检查再擦除带来的另一个问题就是泛型中参数化类型无法支持继承关系,因为泛型的设计初衷就是为了解决 Object 类型转换的弊端而存在,如果泛型中参数化类型支持继承操作就违背了设计的初衷而继续回到原始的 Object 类型转换弊端。也同样可以说这是为了兼容带来的问题,如下:

image

之所以这样我们可以从反面来论证,假设编译不报错则当通过 arrayList2 调用 get() 方法取值时返回的是 String 类型的对象(因为类型检测是根据引用来决定的),而实际上存放的是 Object 类型的对象,这样 get 出来就会 ClassCastException 了,所以这违背了泛型的初衷。对于 arrayList4 同样假设编译不报错,当调用 arrayList4 的 get() 方法取出来的 String 变成了 Object 虽然不会出现 ClassCastException,但是依然没有意义啊,泛型出现的原因就是为了解决类型转换的问题,其次如果我们通过 arrayList4 的 add() 方法继续添加对象则可以添加任意类型对象实例,这就会导致我们 get() 时更加懵逼不知道加的是什么类型了,所以怎么说都是个死循环。

擦除带来的另一个问题就是泛型与多态的冲突,其通过子类中生成桥方法解决了多态冲突问题,这个问题的验证也很简单,可以通过下面的例子说明:

image

上面代码段的运行情况很诧异吧,按理来说 Creater 类被编译擦除后 setValue 方法的参数应该是 Object 类型了,子类 StringCreater 的 setValue 方法参数类型为 String,看起来父子类的这组方法应该是重载关系,所以调用子类的 setValue 方法添加字符串和 Object 类型参数应该都是合法才对,然而从编译来看子类根本没有继承自父类参数为 Object 类型的 setValue 方法,所以说子类的 setValue 方法是对父类的重写而不是重载(从子类添加 @Override 注解没报错也能说明是重写关系)。关于出现上面现象的原理其实我们通过 javap 看下两个类编译后的本质即可:

image

通过编译后的字节码我们可以看到 Creater 泛型类在编译后类型被擦除为 Object,而我们子类的本意是进行重写实现多态,可类型擦除后子类就和多态产生了冲突,所以编译后的字节码里就出现了桥方法来实现多态。可以看到桥方法的参数类型都是 Object,也就是说子类中真正覆盖父类方法的是桥方法,而子类 String 参数 setValue、getValue 方法上的 @Oveerride 注解只是个假象,桥方法的内部实现是直接调用了我们自己重写的那两个方法;不过上面的 setValue 方法是为了解决类型擦除与多态之间的冲突生成的桥方法,而 getValue 是一种协变,之所以子类中 Object getValue() 和 String getValue() 方法可以同时存在是虚拟机内部的一种区分(我们自己写的代码是不允许这样的),因为虚拟机内部是通过参数类型和返回类型来确定一个方法签名的,所以编译器为了实现泛型的多态允许自己做这个看起来不合法的实现,实质还是交给了虚拟机去区别。

先检查再擦除带来的另一个问题就是泛型读取时会进行自动类型转换问题,所以如果调用泛型方法的返回类型被擦除则在调用该方法时插入强制类型转换。

关于这个可以通过 javap 去查看使用 List 的 add、get 方法后的字节码指令,你会发现 checkcast 指令不是在 get 方法里面强转的(虽然 get 方法里面返回值在代码里面是被转换成了 T,实际编译擦除了),而是在调用处强转的。

擦除带来的另一个问题是泛型类型参数不能是基本类型,比如 ArrayList 是不合法的,只有 ArrayList 是合法的,因为当类型擦除后 ArrayList 的原始类型是 Object,而 Object 是引用类型而不是基本类型。

擦除带来的另一个问题是无法进行具体泛型参数类型的运行时类型检查,譬如 arrayList instanceof ArrayList 是非法的,Java 对于泛型运行时检查的支持仅限于 arrayList instanceof ArrayList 方式。

擦除带来的另一个问题是我们不能抛出也不能捕获泛型类的对象,因为异常是在运行时捕获和抛出的,而在编译时泛型信息会被擦除掉,擦除后两个 catch 会变成一样的东西。也不能在 catch 子句中使用泛型变量,因为泛型信息在编译时已经替换为原始类型(譬如 catch(T) 在限定符情况下会变为原始类型 Throwable),如果可以在 catch 子句中使用则违背了异常的捕获优先级顺序。

问:****为什么 Java 的泛型数组不能采用具体的泛型类型进行初始化?

答: 这个问题可以通过一个例子来说明。

image

由于 JVM 泛型的擦除机制,所以上面代码可以给 oa[1] 赋值为 ArrayList 也不会出现异常,但是在取出数据的时候却要做一次类型转换,所以就会出现 ClassCastException,如果可以进行泛型数组的声明则上面说的这种情况在编译期不会出现任何警告和错误,只有在运行时才会出错,但是泛型的出现就是为了消灭 ClassCastException,所以如果 Java 支持泛型数组初始化操作就是搬起石头砸自己的脚。而对于下面的代码来说是成立的:

image

所以说采用通配符的方式初始化泛型数组是允许的,因为对于通配符的方式最后取出数据是要做显式类型转换的,符合预期逻辑。综述就是说Java 的泛型数组初始化时数组类型不能是具体的泛型类型,只能是通配符的形式,因为具体类型会导致可存入任意类型对象,在取出时会发生类型转换异常,会与泛型的设计思想冲突,而通配符形式本来就需要自己强转,符合预期。

关于这道题的答案其 Oracle 官方文档给出了原因:https://docs.oracle.com/javase/tutorial/extra/generics/fineprint.html

问:****下面语句哪些是有问题,哪些没有问题?

image

答: 上面每个语句的问题注释部分已经阐明了,因为在 Java 中是不能创建一个确切的泛型类型的数组的,除非是采用通配符的方式且要做显式类型转换才可以。

问:****如何正确的初始化泛型数组实例?

答: 这个无论我们通过 new ArrayList[10] 的形式还是通过泛型通配符的形式初始化泛型数组实例都是存在警告的,也就是说仅仅语法合格,运行时潜在的风险需要我们自己来承担,因此那些方式初始化泛型数组都不是最优雅的方式,我们在使用到泛型数组的场景下应该尽量使用列表集合替换,此外也可以通过使用 java.lang.reflect.Array.newInstance(Class componentType, int length) 方法来创建一个具有指定类型和维度的数组,如下:

image

所以使用反射来初始化泛型数组算是优雅实现,因为泛型类型 T 在运行时才能被确定下来,我们能创建泛型数组也必然是在 Java 运行时想办法,而运行时能起作用的技术最好的就是反射了。

问:****Java 泛型对象能实例化 T t = new T() 吗,为什么?

答: 不能,因为在 Java 编译期没法确定泛型参数化类型,也就找不到对应的类字节码文件,所以自然就不行了,此外由于 T 被擦除为 Object,如果可以 new T() 则就变成了 new Object(),失去了本意。如果要实例化一个泛型 T 则可以通过反射实现(实例化泛型数组也类似),如下:

image

原因就不解释了,姑且可以认为和上面泛型数组创建一个原因,至于本质深层次原因请关注后边关于泛型反射面试题的推送。
3. 什么是泛型中的限定通配符和非限定通配符 ?

这是另一个非常流行的Java泛型面试题。限定通配符对类型进行了限制。有两种限定通配符,一种是它通过确保类型必须是T的子类来设定类型的上界,另一种是它通过确保类型必须是T的父类来设定类型的下界。泛型类型必须用限定内的类型来进行初始化,否则会导致编译错误。另一方面表示了非限定通配符,因为可以用任意类型来替代。

4. List和List 之间有什么区别 ?

这和上一个面试题有联系,有时面试官会用这个问题来评估你对泛型的理解,而不是直接问你什么是限定通配符和非限定通配符。这两个List的声明都是限定通配符的例子,List可以接受任何继承自T的类型的List,而List可以接受任何T的父类构成的List。例如List可以接受List或List。在本段出现的连接中可以找到更多信息。

5. 如何编写一个泛型方法,让它能接受泛型参数并返回泛型类型?

编写泛型方法并不困难,你需要用泛型类型来替代原始类型,比如使用T, E or K,V等被广泛认可的类型占位符。泛型方法的例子请参阅Java集合类框架。最简单的情况下,一个泛型方法可能会像这样:

public V put(K key, V value) {

return cache.put(key, value);

}

image

6. Java中如何使用泛型编写带有参数的类?

这是上一道面试题的延伸。面试官可能会要求你用泛型编写一个类型安全的类,而不是编写一个泛型方法。关键仍然是使用泛型类型来代替原始类型,而且要使用JDK中采用的标准占位符。

7. 编写一段泛型程序来实现LRU缓存?

对于喜欢Java编程的人来说这相当于是一次练习。给你个提示,LinkedHashMap可以用来实现固定大小的LRU缓存,当LRU缓存已经满了的时候,它会把最老的键值对移出缓存。

LinkedHashMap提供了一个称为removeEldestEntry()的方法,该方法会被put()和putAll()调用来删除最老的键值对。当然,如果你已经编写了一个可运行的JUnit测试,你也可以随意编写你自己的实现代码。

8. 你可以把List传递给一个接受List参数的方法吗?

对任何一个不太熟悉泛型的人来说,这个Java泛型题目看起来令人疑惑,因为乍看起来String是一种Object,所以List应当可以用在需要List的地方,但是事实并非如此。真这样做的话会导致编译错误。

如果你再深一步考虑,你会发现Java这样做是有意义的,因为List可以存储任何类型的对象包括String, Integer等等,而List却只能用来存储Strings。

List objectList;
List stringList;
objectList = stringList;

9. Array中可以用泛型吗?

这可能是Java泛型面试题中最简单的一个了,当然前提是你要知道Array事实上并不支持泛型,这也是为什么Joshua Bloch在Effective Java一书中建议使用List来代替Array,因为List可以提供编译期的类型安全保证,而Array却不能。

10. 如何阻止Java中的类型未检查的警告?

如果你把泛型和原始类型混合起来使用,例如下列代码,Java 5的javac编译器会产生类型未检查的警告,例如

List rawList = new ArrayList();

你可能感兴趣的:(Java泛型面试题)