资深架构师和你谈谈 Kotlin 的泛型

本文深入探讨了 Kotlin 泛型的原理和使用,包括类型安全、类型擦除、上界通配符、下界通配符、out 和 in 关键字、通配符 * 以及泛型方法。通过与 Java 泛型的对比,解释了 Kotlin 中泛型的协变和逆变,并介绍了 reified 和 inline 的应用,帮助开发者更好地理解和运用 Kotlin 泛型。
摘要由CSDN通过智能技术生成

引子

相信总是有很多人,总是在抱怨泛型无论怎么学习,都只是停留在一个简单使用的水平,所以一直为此而备受苦恼。

Kotlin 作为一门能和 Java 相互调用的语言,自然也支持泛型,不过 Kotlin 的新关键字 in 和 out 却总能绕晕一部分人,归根结底,还是因为 Java 的泛型基本功没有足够扎实。

很多人总是会产生这些疑问:

  • Kotlin 泛型和 Java 泛型到底有何区别?
  • Java 泛型存在的意义到底是什么?
  • Java 的类型擦除到底是指什么?
  • Java 泛型的上界、下界、通配符到底有何区别?它们可以实现多重限制么?
  • Java 的 <? extends T>、<? super T>、<?> 到底对应了什么?有哪些使用场景?
  • Kotlin 的 in、out、*、where 到底有何魔力?
  • 泛型方法又是什么?

今天,就用一篇文章为大家解除上述疑惑。

泛型:类型安全的利刃

众所周知,Java 在 1.5 之前,是没有泛型这个概念的。那时候的 List 还只是一个可以装下一切的集合。所以我们难免会写上这样的代码:

List list = new ArrayList();
list.add(1);
list.add("nanchen2251");
String str = (String) list.get(0);

上面的代码编译并没有任何问题,但运行的时候一定会出现常见的 ClassCastException 异常:

Exception in thread "main" java.lang.ClassCastException: java.lang.Integer cannot be cast to java.lang.String

这个体验非常糟糕,我们真正需要的是在代码编译的时候就能发现错误,而不是让错误的代码发布到生产环境中。

而如果上述代码我们增加上泛型,就会在编译期就能看到明显的错误啦。

List<String> list = new ArrayList<>();
list.add(1);
//  报错 Required type:String but Provided:int
list.add("nanchen2251");
String str = list.get(0);

很明显,泛型的出现,让类型更加安全,使我们在使用 List、Map 等不再需要去专门编写 StringList、StringMap 了,只需要在声明 List 的同时指定参数类型即可。

总的来说,泛型具备以下优势:

  • 类型检查,能在编译时就帮开发检查出错误;
  • 更加语义化,比如我们声明一个 LIst<String>,我们可以很直接知道里面存储的是 String 对象;
  • 能自动进行类型转换,获取数据的时候不需要再做强转操作;
  • 能写出更加通用化的代码。

类型擦除

可能有些同学思考过这样一个问题,既然泛型是和类型相关的,那么是不是也能使用类型的多态呢?

我们知道,一个子类型是可以赋值给父类型的,比如:

Object obj = "nanchen2251";
//  这是多态

Object 作为 String 的父类,自然可以接受 String 对象的赋值,这样的代码我们早已司空见惯,并没有什么问题。

但当我们写下这串代码:

List<String> list = new ArrayList<String>();
List<Object> objects = list;  
//  多态用在这里会报错 Required type:List<Object> Provided: List<String>

上面发生了赋值错误,这是因为 Java 的泛型本身具有「不可变性 Invariance」,Java 里面认为 List<String> 和 List<Object> 类型并不一致,也就是说,子类的泛型 List<String> 不属于泛型 List<Object> 的子类。

由于 Java 的泛型本身是一种 「伪泛型」,Java 为了兼容 1.5 以前的版本,不得以在泛型底层实现上使用 Object 引用,所以我们声明的泛型在编译时会发生「类型擦除」,泛型类型会被 Object 类型取代。比如:

class Demo<T> {
    void func(T t){
        // ...
    }
}

会被编译成:

class Demo {
    void func(Object t){
        // ...     
    }
}

可能你会好奇,在编译时发生类型擦除后,我们的泛型都被更换成了 Object,那为什么我们在使用的时候,却不需要强转操作呢?比如:

List<String> list = new ArrayList<>();
list.add("nanchen2251");
String str = list.get(0);
//  这里并没有要求我们把 list.get(0) 强转为 String

这是因为编译器会根据我们声明的泛型类型进行提前的类型检查,然后再进行类型擦除,擦除为 Object,但在字节码中其实还存储了我们的泛型的类型信息,在使用到泛型类型的时候会把擦除后的 Object 自动做类型强转操作。所以上面的 list.get(0) 本身就是一个经过强转的 String 对象了。

这个技术看起来还蛮好

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值