本文灵感来源:群友提出泛型相关的问题,感觉很多人对泛型并不是很了解~
Kotlin中的泛型和Java中的泛型其实大同小异,只是语法稍微有些不同。
大部分内容摘取自:
《Kotlin实用指南》
,感兴趣的可以订阅一波 ~
0x1、要点提炼
什么是泛型
→「
将确定不变的类型参数化,保证集合中存储元素都是同一种
」
泛型的好处
→「
编译期类型安全检查
」和「
消除强类型转换,提高可读性
」
Java中泛型的使用
→
泛型类
、
泛型接口
和
泛型方法
;
Java假泛型实现原理
→「
类型擦除(Type Erasure)
」只存在与编译期,进JVM前会擦除类型
Java有界泛型类型
→
限制允许实例化的泛型类型
,「
extends 父类型
」
Java通配符
→
<T>与<?>
的区别(
定义-形参
,
实例化-实参
)
上边界通配符
→
<? extends 父类型>
,
父类型未知
,只能读不能写,
协变
下边界通配符
→
<? super 子类型>
,
子类型未知
,只能写不能读,
逆变
无限定通配符
→
<?>
,等价于
<? extends Object>
,类型完全未知,只能读不能写,
不变
Kotlin中的型变
→「
声明处型变
」
out(协变)与in(逆变)
,
类型映射
<*>
Kotlin获取泛型类型
→
匿名内部类
,
反射
,
实例化类型参数代替类引用
,
内联函数
0x2、什么是泛型
0x3、Java中泛型的使用
泛型可用于类、接口和方法的创建,对应:
泛型类
,
泛型接口
和
泛型方法
,代码示例如下:
注意事项
:
1、泛型的参数只能是「
类类型
」,不能是简单数据类型(int, float等);
2、泛型可以有多个泛型参数
Tips
:
泛型类型的命名不是必须为T
,也可以使用其他「
单个大写字母
」,没有强制的命名规范,但为了便于阅读,有一些约定成俗的命名规范:
通用泛型类型:T,S,U,V
集合元素泛型类型:E
映射键-值泛型类型:K,V
数值泛型类型:N
0x4、Java假泛型实现原理
和C#中的泛型不同,Java和Kotlin中的泛型都是假泛型,实现原理就是「
类型擦除
(Type Erasure)」。
Java编译器在生成
Java字节码中是不包含泛型中的类型信息的
,只存在于
代码编译阶段
,进JVM前会被擦除。
不信?写个简单的代码体验下:
运行结果如下
:
从输出结果可以看到获得的类型确实被擦除了,此时的「
类类型
」皆为
ArrayList
;
问 → 那我们定义的
泛型类型(Integer, String)
到底去哪了?
答 → 被替换成了「
原始类型
」(字节码中的真正类型)
问 → 那原始类型具体是什么类型?
答 → 「
限定类型
」,无限定的话都用
Object
替换。
问 → ???
答 → 且听我娓娓道来~
还是上面的代码,进Java字节码看看(
View -> Show Bytecode
)
的确,Integer和String都被替换成了Object,那这个「
限定类型
」呢?写个例子试试:
看下
字节码
:
行吧,此时的「
原始类型
」为「
限定类型
」即
Animal
类。
没限定类型的,都替换成Object类,也使得我们可以通过一些操作,
绕过泛型
。
比如,我们利用「
反射
」往Integer类型的List插入一个String值是不会报错的:
运行结果如下
:
0x5、Java有界泛型类型
如果不对泛型类型做限制,
泛型类型可实例化为任意类型
的话,可能会产生某些安全隐患。为了限制允许实例化的泛型类型,可在泛型类型后追加
extends 父类型
,代码示例如下:
有界泛型类型在一定程度上限制了泛型类型,提高了程序的安全性;因为定义了边界,所以
可以调用父类或父接口的方法
。
0x6、Java通配符
Java泛型本身「
不变
」的,不支持「
协变
」和「
逆变
」,是通过通配符(?)来实现的
① <T>与<?>的区别
<T>
「
泛型标识符
」用于泛型「
定义时
」可理解为「
形参
」;
<?>
「
通配符
」用于泛型「
实例化时
」可理解为「
实参
」。
代码示例如下
:
② 各种通配符
上边界通配符<? extends 父类型>
实例化时可确定为「
父类型的未知类型
」故「
只能读不能写
」,从而使得类型是「
协变的
」。
代码示例如下
:
下边界通配符:<? super 子类型>
实例化时可确定为「
子类型的未知类型
」故「
只能写不能读
」从而使得类型是「
逆变的
」。
(不能读指的是
不能按照泛型类型读取
)<
代码示例如下:
上边界通配符只能读不能写,改为下边界通配符
无限定通配符:<?>
等同于上边界通配符
<? extends Object>
,「
类型完全未知
」故「
只能读不能写
」从而使得类型是「
不变的
」。
Tips
:看完这里,估计有部分读者还是有点懵逼,这里总结下:
不变
→ 子类和父类没关系,正常存取,不用通配符就好,Java集合本身就是不变的;
协变
→ A是B的子类型,泛型<A>也是泛型<B>的子类型,只想取,用extends。
逆变
→ A是B的子类型,泛型<B>也是泛型<A>的子类型,只想存,用super。
PECS法则
(Producer Extends,Consumer Super)
参数化类型
是一个生产者,则使用:<? extends T> 如果它是一个消费者,则使用<? super T>
0x7、Kotlin中的型变
和Java泛型一样,Kotlin中的泛型也是「
不变的
」,没有「
通配符类型
」,但有两个其他的东西:「
声明处型变
」(declaration-site variance) 与 「
类型投影
」(type projections)
① 声明处型变
其实就是用「
out
」和「
in
」关键字来替换
「
out
」
协变
,等同于Java中的<? extends>,
能读不能写
,代码示例如下:
「
in
」
逆变
,等同于Java中的<? super>,
能写不能读
,代码示例如下:
② 类型投影
其实就是对应Java中的*通配符:
Java中<?>等同于<* extends Object>
Kotlin中
<*>
等同于
out Any
0x7、Kotlin获取泛型类型
在Kotlin中可以通过下述四种方法获取泛型的类型(前两种Java也适用):
① 匿名内部类
原理:
匿名内部类的声明在编译时进行,实例化在运行时进行
。
代码示例如下
:
获取运行时泛型参数类型
,子类可获得父类泛型的具体类型。
代码示例如下
:
③ 实例化类型参数代替类引用
定义一个扩展函数用于启动Activity,代码示例如下:
fun <T: Activity> FragmentActivity.startActivity(context: Context, clazz: Class<T>) {
startActivity(Intent(context, clazz))
startActivity(context, MainActivity::class.java)
④ 内联函数
Kotlin中使用「inline」关键字定义一个内联函数,配合「reified」具体化(类型不擦除),得到使用泛型类型的Class。修改后的代码示例如下:
inline fun <reified T : Activity> Activity.startActivity(context: Context) {
startActivity(Intent(context, T::class.java))
startActivity<MainActivity>(context)
参考文献:
《Java泛型学习系列-绪论》
《Android 进阶:基于 Kotlin 的 Android App 开发实践》