最新一文读懂 Java 和 Kotlin 的泛型难点,聪明人已经收藏了

写在最后

还有一份JAVA核心知识点整理(PDF):JVM,JAVA集合,JAVA多线程并发,JAVA基础,Spring原理,微服务,Netty与RPC,网络,日志,Zookeeper,Kafka,RabbitMQ,Hbase,MongoDB,Cassandra,设计模式,负载均衡,数据库,一致性哈希,JAVA算法,数据结构,加密算法,分布式缓存,Hadoop,Spark,Storm,YARN,机器学习,云计算…

image

本文已被CODING开源项目:【一线大厂Java面试题解析+核心总结学习笔记+最新讲解视频+实战项目源码】收录

需要这份系统化的资料的朋友,可以点击这里获取

复制代码

总的来说,泛型有以下几点优势:

  • 类型检查,在编译阶段就能发现错误

  • 更加语义化,看到 List<String>我们就知道存储的数据类型是 String

  • 自动类型转换,在取值时无需进行手动类型转换

  • 能够将逻辑抽象出来,使得代码更加具有通用性

三、类型擦除

泛型是在 Java 5 版本开始引入的,所以在 Java 4 中 ArrayList 还不属于泛型类,其内部通过 Object 向上转型外部强制类型转换来实现数据存储和逻辑复用,此时开发者的项目中已经充斥了大量以下类型的代码:

List stringList = new ArrayList();

stringList.add(“业志陈”);

stringList.add(“https://juejin.cn/user/923245496518439”);

String str = (String) stringList.get(0);

复制代码

而在推出泛型的同时,Java 官方也必须保证二进制的向后兼容性,用 Java 4 编译出的 Class 文件也必须能够在 Java 5 上正常运行,即 Java 5 必须保证以下两种类型的代码能够在 Java 5 上共存且正常运行

List stringList = new ArrayList();

List stringList = new ArrayList();

复制代码

为了实现这一目的,Java 就通过类型擦除这种比较别扭的方式来实现泛型。编译器在编译时会擦除类型实参,在运行时不存在任何类型相关的信息,泛型对于 JVM 来说是透明的,有泛型和没有泛型的代码通过编译器编译后所生成的二进制代码是完全相同的

例如,分别声明两个泛型类和非泛型类,拿到其 class 文件

public class GenericTest {

public static class NodeA {

private Object obj;

public NodeA(Object obj) {

this.obj = obj;

}

}

public static class NodeB {

private T obj;

public NodeB(T obj) {

this.obj = obj;

}

}

public static void main(String[] args) {

NodeA nodeA = new NodeA(“业志陈”);

NodeB nodeB = new NodeB<>(“业志陈”);

System.out.println(nodeB.obj);

}

}

复制代码

可以看到 NodeA 和 NodeB 两个对象对应的字节码其实是完全一样的,最终都是使用 Object 来承载数据,就好像传递给 NodeB 的类型参数 String 不见了一样,这便是类型擦除

public class generic.GenericTest {

public generic.GenericTest();

Code:

0: aload_0

1: invokespecial #1 // Method java/lang/Object.“”😦)V

4: return

public static void main(java.lang.String[]);

Code:

0: new #2 // class generic/GenericTest$NodeA

3: dup

4: ldc #3 // String 业志陈

6: invokespecial #4 // Method generic/GenericTest$NodeA.“”:(Ljava/lang/Object;)V

9: astore_1

10: new #5 // class generic/GenericTest$NodeB

13: dup

14: ldc #3 // String 业志陈

16: invokespecial #6 // Method generic/GenericTest$NodeB.“”:(Ljava/lang/Object;)V

19: astore_2

20: getstatic #7 // Field java/lang/System.out:Ljava/io/PrintStream;

23: aload_2

24: invokestatic #8 // Method generic/GenericTest$NodeB.access 000 : ( L g e n e r i c / G e n e r i c T e s t 000:(Lgeneric/GenericTest 000:(Lgeneric/GenericTestNodeB;)Ljava/lang/Object;

27: checkcast #9 // class java/lang/String

30: invokevirtual #10 // Method java/io/PrintStream.println:(Ljava/lang/String;)V

33: return

}

复制代码

而如果让 NodeA 直接使用 String 类型,并且为泛型类 NodeB 设定上界约束 String,两者的字节码也会完全一样

public class GenericTest {

public static class NodeA {

private String obj;

public NodeA(String obj) {

this.obj = obj;

}

}

public static class NodeB {

private T obj;

public NodeB(T obj) {

this.obj = obj;

}

}

public static void main(String[] args) {

NodeA nodeA = new NodeA(“业志陈”);

NodeB nodeB = new NodeB<>(“业志陈”);

System.out.println(nodeB.obj);

}

}

复制代码

可以看到 NodeA 和 NodeB 的字节码是完全相同的

public class generic.GenericTest {

public generic.GenericTest();

Code:

0: aload_0

1: invokespecial #1 // Method java/lang/Object.“”😦)V

4: return

public static void main(java.lang.String[]);

Code:

0: new #2 // class generic/GenericTest$NodeA

3: dup

4: ldc #3 // String 业志陈

6: invokespecial #4 // Method generic/GenericTest$NodeA.“”:(Ljava/lang/String;)V

9: astore_1

10: new #5 // class generic/GenericTest$NodeB

13: dup

14: ldc #3 // String 业志陈

16: invokespecial #6 // Method generic/GenericTest$NodeB.“”:(Ljava/lang/String;)V

19: astore_2

20: getstatic #7 // Field java/lang/System.out:Ljava/io/PrintStream;

23: aload_2

24: invokestatic #8 // Method generic/GenericTest$NodeB.access 000 : ( L g e n e r i c / G e n e r i c T e s t 000:(Lgeneric/GenericTest 000:(Lgeneric/GenericTestNodeB;)Ljava/lang/String;

27: invokevirtual #9 // Method java/io/PrintStream.println:(Ljava/lang/String;)V

30: return

}

复制代码

所以说,当泛型类型被擦除后有两种转换方式

  • 如果泛型没有设置上界约束,那么将泛型转化成 Object 类型

  • 如果泛型设置了上界约束,那么将泛型转化成该上界约束

该结论也可以通过反射泛型类的 Class 对象来验证

public class GenericTest {

public static class NodeA {

private T obj;

public NodeA(T obj) {

this.obj = obj;

}

}

public static class NodeB {

private T obj;

public NodeB(T obj) {

this.obj = obj;

}

}

public static void main(String[] args) {

NodeA nodeA = new NodeA<>(“业志陈”);

getField(nodeA.getClass());

NodeB nodeB = new NodeB<>(“https://juejin.cn/user/923245496518439”);

getField(nodeB.getClass());

}

private static void getField(Class clazz) {

for (Field field : clazz.getDeclaredFields()) {

System.out.println("fieldName: " + field.getName());

System.out.println("fieldTypeName: " + field.getType().getName());

}

}

}

复制代码

NodeA 对应的是 Object,NodeB 对应的是 String

fieldName: obj

fieldTypeName: java.lang.Object

fieldName: obj

fieldTypeName: java.lang.String

复制代码

那既然在运行时不存在任何类型相关的信息,泛型又为什么能够实现类型检查类型自动转换等功能呢?

其实,类型检查是编译器在编译前帮我们完成的,编译器知道我们声明的具体的类型实参,所以类型擦除并不影响类型检查功能。而类型自动转换其实是通过内部强制类型转换来实现的,上面给出的字节码中也可以看到有一条类型强转 checkcast 的语句

27: checkcast #9 // class java/lang/String

复制代码

例如,ArrayList 内部虽然用于存储数据的是 Object 数组,但 get 方法内部会自动完成类型强转

transient Object[] elementData;

public E get(int index) {

rangeCheck(index);

return elementData(index);

}

@SuppressWarnings(“unchecked”)

E elementData(int index) {

//强制类型转换

return (E) elementData[index];

}

复制代码

所以 Java 的泛型可以看做是一种特殊的语法糖,因此也被人称为伪泛型

四、类型擦除的后遗症

Java 泛型对于类型的约束只在编译期存在,运行时仍然会按照 Java 5 之前的机制来运行,泛型的具体类型在运行时已经被删除了,所以 JVM 是识别不到我们在代码中指定的具体的泛型类型的

例如,虽然List<String>只能用于添加字符串,但我们只能泛化地识别到它属于List<?>类型,而无法具体判断出该 List 内部包含的具体类型

List stringList = new ArrayList<>();

//正常

if (stringList instanceof ArrayList<?>) {

}

//报错

if (stringList instanceof ArrayList) {

}

复制代码

我们只能对具体的对象实例进行类型校验,但无法判断出泛型形参的具体类型

public void filter(T data) {

//正常

if (data instanceof String) {

}

//报错

if (T instanceof String) {

}

//报错

Class tClass = T::getClass;

}

复制代码

此外,类型擦除也会导致 Java 中出现多态问题。例如,以下两个方法的方法签名并不完全相同,但由于类型擦除的原因,入参参数的数据类型都会被看成 List<Object>,从而导致两者无法共存在同一个区域内

public void filter(List stringList) {

}

public void filter(List stringList) {

}

复制代码

五、Kotlin 泛型

Kotlin 泛型在大体上和 Java 一致,毕竟两者需要保证兼容性

class Plate(val t: T) {

fun cut() {

println(t.toString())

}

}

class Apple

class Banana

fun main() {

val plateApple = Plate(Apple())

//泛型类型自动推导

val plateBanana = Plate(Banana())

plateApple.cut()

plateBanana.cut()

}

复制代码

Kotlin 也支持在扩展函数中使用泛型

fun List.find(t: T): T? {

val index = indexOf(t)

return if (index > -1) get(index) else null

}

复制代码

需要注意的是,为了实现向后兼容,目前高版本 Java 依然允许实例化没有具体类型参数的泛型类,这可以说是一个对新版本 JDK 危险但对旧版本友好的兼容措施。但 Kotlin 要求在使用泛型时需要显式声明泛型类型或者是编译器能够类型推导出具体类型,任何不具备具体泛型类型的泛型类都无法被实例化。因为 Kotlin 一开始就是基于 Java 6 版本的,一开始就存在了泛型,自然就不存在需要兼容老代码的问题,因此以下例子和 Java 会有不同的表现

val arrayList1 = ArrayList() //错误,编译器报错

val arrayList2 = arrayListOf() //正常

val arrayList3 = arrayListOf(1, 2, 3) //正常

复制代码

还有一个比较容易让人误解的点。我们经常会使用 asas? 来进行类型转换,但如果转换对象是泛型类型的话,那就会由于类型擦除而出现误判。如果转换对象有正确的基础类型,那么转换就会成功,而不管类型实参是否相符。因为在运行时转换发生的时候类型实参是未知的,此时编译器只会发出 “unchecked cast” 警告,代码还是可以正常编译的

例如,在以下例子中代码的运行结果还符合我们的预知。第一个转换操作由于类型相符,所以打印出了相加值。第二个转换操作由于基础类型是 Set 而非 List,所以抛出了 IllegalAccessException

fun main() {

printSum(listOf(1, 2, 3)) //6

printSum(setOf(1, 2, 3)) //IllegalAccessException

}

fun printSum(c: Collection<*>) {

val intList = c as? List ?: throw IllegalAccessException(“List is expected”)

println(intList.sum())

}

复制代码

而在以下例子中抛出的却是 ClassCastException,这是因为在运行时不会判断且无法判断出类型实参到底是否是 Int,而只会判断基础类型 List 是否相符,所以 as? 操作会成功,等到要执行相加操作时才会发现拿到的是 String 而非 Number

printSum(listOf(“1”, “2”, “3”))

Exception in thread “main” java.lang.ClassCastException: java.lang.String cannot be cast to java.lang.Number

复制代码

六、上界约束

泛型本身已经带有类型约束的作用,我们也可以进一步细化其支持的具体类型

例如,假设存在一个盘子 Plate,我们要求该 Plate 只能用于装水果 Fruit,那么就可以对其泛型声明做进一步约束,Java 中使用 extend 关键字来声明约束规则,而 Kotlin 使用的是 : 。这样 Plate 就只能用于 Fruit 和其子类,而无法用于 Noodles 等不相关的类型,这种类型约束就被称为上界约束

open class Fruit

class Apple : Fruit()

class Noodles

class Plate(val t: T)

fun main() {

val applePlate = Plate(Apple()) //正常

val noodlesPlate = Plate(Noodles()) //报错

}

复制代码

如果上界约束拥有多层类型元素,Java 是使用 & 符号进行链式声明,Kotlin 则是用 where 关键字来依次进行声明

interface Soft

class Plate(val t: T) where T : Fruit, T : Soft

open class Fruit

class Apple : Fruit()

class Banana : Fruit(), Soft

fun main() {

val applePlate = Plate(Apple()) //报错

val bananaPlate = Plate(Banana()) //正常

}

复制代码

此外,没有指定上界约束的类型形参会默认使用 Any? 作为上界,即我们可以使用 String 或 String? 作为具体的类型实参。如果想确保最终的类型实参一定是非空类型,那么就需要主动声明上界约束为 Any

七、类型通配符 & 星号投影

假设现在有个需求,需要我们提供一个方法用于遍历所有类型的 List 集合并打印元素

第一种做法就是直接将方法参数类型声明为 List,不包含任何泛型类型声明。这种做法可行,但编译器会警告无法确定 list元素的具体类型,所以这不是最优解法

public static void printList1(List list) {

for (Object o : list) {

System.out.println(o);

}

}

复制代码

可能会想到的第二种做法是:将泛型类型直接声明为 Object,希望让其适用于任何类型的 List。这种做法完全不可行,因为即使 StringObject 的子类,但 List<String>List<Object>并不具备从属关系,这导致 printList2 方法实际上只能用于List<Object>这一种具体类型

public static void printList2(List list) {

for (Object o : list) {

System.out.println(o);

}

}

复制代码

最优解法就是要用到 Java 的类型通配符 ? 了,printList3方法完全可行且编译器也不会警告报错

public static void printList3(List<?> list) {

for (Object o : list) {

System.out.println(o);

}

}

复制代码

? 表示我们并不关心具体的泛型类型,而只是想配合其它类型进行一些条件限制。例如,printList3方法希望传入的是一个 List,但不限制泛型的具体类型,此时List<?>就达到了这一层限制条件

类型通配符也存在着一些限制。因为 printList3 方法并不包含具体的泛型类型,所以我们从中取出的值只能是 Object 类型,且无法向其插入值,这都是为了避免发生 ClassCastException

Java 的类型通配符对应 Kotlin 中的概念就是**星号投影 * **,Java 存在的限制在 Kotlin 中一样有

fun printList(list: List<*>) {

for (any in list) {

println(any)

}

}

复制代码

此外,星号投影只能出现在类型形参的位置,不能作为类型实参

val list: MutableList<*> = ArrayList() //正常

val list2: MutableList<> = ArrayList<>() //报错

复制代码

八、协变 & 不变

看以下例子。Apple 和 Banana 都是 Fruit 的子类,可以发现 Apple[] 类型的对象是可以赋值给 Fruit[] 的,且 Fruit[] 可以容纳 Apple 对象和 Banana 对象,这种设计就被称为协变,即如果 A 是 B 的子类,那么 A[] 就是 B[] 的子类型。相对的,Object[] 就是所有数组对象的父类型

static class Fruit {

}

static class Apple extends Fruit {

}

static class Banana extends Fruit {

}

public static void main(String[] args) {

Fruit[] fruitArray = new Apple[10];

//正常

fruitArray[0] = new Apple();

//编译时正常,运行时抛出 ArrayStoreException

fruitArray[1] = new Banana();

}

复制代码

而 Java 中的泛型是不变的,这意味着 String 虽然是 Object 的子类,但List<String>并不是List<Object>的子类型,两者并不具备继承关系

List stringList = new ArrayList<>();

List objectList = stringList; //报错

复制代码

那为什么 Java 中的泛型是不变的呢?

这可以通过看一个例子来解释。假设 Java 中的泛型是协变的,那么以下代码就可以成功通过编译阶段的检查,在运行时就不可避免地将抛出 ClassCastException,而引入泛型的初衷就是为了实现类型安全,支持协变的话那泛型也就没有比数组安全多少了,因此就将泛型被设计为不变

List strList = new ArrayList<>();

List objs = strList; //假设可以运行,实际上编译器会报错

objs.add(1);

String str = strList.get(0); //将抛出 ClassCastException,无法将整数转换为字符串

复制代码

再来想个问题,既然协变本身并不安全,那么数组为何又要被设计为协变呢?

最后

针对最近很多人都在面试,我这边也整理了相当多的面试专题资料,也有其他大厂的面经。希望可以帮助到大家。

下面的面试题答案都整理成文档笔记。也还整理了一些面试资料&最新2021收集的一些大厂的面试真题(都整理成文档,小部分截图)

在这里插入图片描述

最新整理电子书

在这里插入图片描述

本文已被CODING开源项目:【一线大厂Java面试题解析+核心总结学习笔记+最新讲解视频+实战项目源码】收录

需要这份系统化的资料的朋友,可以点击这里获取

ana();

}

复制代码

而 Java 中的泛型是不变的,这意味着 String 虽然是 Object 的子类,但List<String>并不是List<Object>的子类型,两者并不具备继承关系

List stringList = new ArrayList<>();

List objectList = stringList; //报错

复制代码

那为什么 Java 中的泛型是不变的呢?

这可以通过看一个例子来解释。假设 Java 中的泛型是协变的,那么以下代码就可以成功通过编译阶段的检查,在运行时就不可避免地将抛出 ClassCastException,而引入泛型的初衷就是为了实现类型安全,支持协变的话那泛型也就没有比数组安全多少了,因此就将泛型被设计为不变

List strList = new ArrayList<>();

List objs = strList; //假设可以运行,实际上编译器会报错

objs.add(1);

String str = strList.get(0); //将抛出 ClassCastException,无法将整数转换为字符串

复制代码

再来想个问题,既然协变本身并不安全,那么数组为何又要被设计为协变呢?

最后

针对最近很多人都在面试,我这边也整理了相当多的面试专题资料,也有其他大厂的面经。希望可以帮助到大家。

下面的面试题答案都整理成文档笔记。也还整理了一些面试资料&最新2021收集的一些大厂的面试真题(都整理成文档,小部分截图)

[外链图片转存中…(img-OHXo0t2I-1715658430175)]

最新整理电子书

[外链图片转存中…(img-BavaLhlD-1715658430175)]

本文已被CODING开源项目:【一线大厂Java面试题解析+核心总结学习笔记+最新讲解视频+实战项目源码】收录

需要这份系统化的资料的朋友,可以点击这里获取

  • 3
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值