kotlin的泛型基础和 java 很像, 所以我建议学习 kotlin 的泛型前, 先去学习下 java 的泛型, 至少搞懂通配符,
<? extends X>
和<? super X>
是怎么回事, 怎么写 泛型函数, 泛型类, 知道泛型的本质是什么?
泛型
对于我来说, 我从c++转过来的, 在 c++ 中并没有什么泛型, 只有模板, c++ 根据编译期间所遇到的类型, 按照模板生成代码, 每种类型的模板生成一段代码(不知道现在还是不是这样实现, 我只学到 c++11), 但从中我体会到泛型其实不过和函数参数一样, 函数的()
内传递的是参数, 而泛型的 <>
传递的是类型. 说白了都是参数, 只不过一个是变量另一个是类型
函数, 参数, 属性和类的泛型
fun <T> print(t: T) {
println(t)
}
class GenericsDemo01<T>(val f: T) {
fun print(t: T) {
println(t)
}
}
复制代码
泛型约束
很多时候我们需要将泛型的类型约束在某个界限, 比如: sum函数的泛型
fun <T> sum(a: T, b: T): T {
return a + b // error
}
复制代码
参数 a
和 参数 b
并不是什么类型都支持 +
这项操作, 所以我们需要对传入的类型参数(泛型)做限制, 像下面这样
fun <T : Integer> sum(a: T, b: T): T {
return a + b
}
复制代码
这样操作类似于 java 的 <T extends Integer>
, 限定 T
必须继承 Integer
(或者说T
必须是Integer
的子类).
java 的
<T extends Integer>
用于集合的泛型, 而泛型约束通常用于非集合的泛型, 因为集合泛型已经有 协变和逆变 的约束了, 不需要这一章的泛型约束
对的, 这样做就不会出现传入俩 Any 类型
的 a
和 b
做加法运算符这样尴尬的事情
泛型约束不会像集合泛型约束那样严格控制 T 必须是同一个, 你可以这样使用:
private fun <T : Number> printT(a: T, b: T) {
// a = 9999, b = 100.5
println("a = $a, b = $b")
// aClass = class java.lang.Integer, bClass = class java.lang.Double
println("aClass = ${a.javaClass}, bClass = ${b.javaClass}")
}
fun main() {
printT(9999, 100.5)
}
复制代码
a: T, b: T
中的 T
是两个不一样的类型, 一个是 Integer
, 另一个是 Double
上面那段代码类似于 java 的这段代码
static <T extends Number> void printT(T a, T b) {
System.out.println("a = " + a);
System.out.println("b = " + b);
System.out.println(a.getClass());
System.out.println(b.getClass());
}
public static void main(String[] args) throws Exception {
printT(10, 20.1);
}
复制代码
课外: 突然发现 不知道 泛型如何 写 sum 了, 所以想了下, 好像只能使用反射来实现
扫描二维码关注公众号,回复: 13168481 查看本文章![]()
fun <T> sum(a: T, b: T): T {
val clazz = a.javaClass
val sum = clazz.declaredMethods.firstOrNull { it.name == "sum" } ?: throw Exception("can't find function. T not a subclass of Number")
return sum.invoke(a, a, b) as T
}
复制代码
小笔记: 在反射获取
sum
函数时, 发现它的函数签名是:int sum(int, int)
但是我们a: T
类型的T
会被认为是Integer
(泛型只能是Integer
), 而获取sum
却需要int
比较麻烦, 最后发现Integer.TYPE
是拆包类型int
可以考虑从这里下手
为一个泛型添加多个约束
where 类似于 sql 语句的 where 一样
fun <T> ensureTrailingPeriod(seq: T): T where T : CharSequence, T : Appendable {
if (!seq.endsWith(".")) {
seq.append(".")
}
return seq
}
复制代码
约束的好处不仅仅是让我们知道我们需要的类必须是约束和约束的子类. 同时还会让我们的
T
多出很多约束类的函数(包括扩展函数等)
endsWith
函数是CharSequence
的扩展函数,append
是Appendable
接口的函数
前面的 sum
函数也是
java中也允许多约束泛型
static <T extends CharSequence & Appendable> T ensureTrailingPeriod(T seq)
复制代码
泛型类型可以为 null 也可以为 non-null
泛型的类型T
类似于平台类型, 是否为空由程序员决定, kotlin 不再进行可空管理, 程序员认为他是 可空类型 它就是可空类型, 程序员认为它非空类型, 它就是非空类型
fun <T> print(t: T) {
t?.let { println(it) }
}
复制代码
泛型运行时的类型擦除和实化类型
和 java 一样, 泛型在运行时类型擦除, 在编译期间存在类型, 在 运行期间当作 Any 就好
fun <T> isIntList(list: List<T>) {
if (list is List<Int>) { // 这里报错
println("这是错误的")
}
}
fun main() {
val list = listOf(1, 2, 3)
isIntList(list)
}
复制代码
但是可以这样:
if (list is List<*>)
复制代码
kotlin 编译器可以判断在同一个作用域内的泛型类型
val list = listOf(1, 2, 3)
if (list is List<Int>) {
println("这样是可以的")
}
复制代码
下面这种情况也会出现问题
实化类型参数 reified T
在运行期间, 类型被当作 Any
类型, 但它想被强制转换成 T
类型明显是不行的, 这种情况下, 可以考虑使用 inline
和 reified
配合实现
inline fun <reified T> isA(value: Any) = value is T
fun main() {
val a: Int = 10
println(isA<Int>(a))
}
复制代码
前面我们学过, inline
内联的话, 会把代码拷贝到所有调用的地方, 使用上面这种方式kotlin编译器在运行期间
可以识别到泛型的类型
inline
在之前的章节中是为了提高性能, 消除lambda参数带来的副作用对象而使用的, 在本章节是为了实化类型, 这是第二个inline
的使用场景
实例化参数的另一种使用场景是, 将 类型做参数传递后, 借助该类型获取 Class
类对象
inline fun <reified T> loadService(): ServiceLoader<T>? {
return ServiceLoader.load(T::class.java)
}
fun main() {
val loadService = loadService<Int>()
}
复制代码
变型: 泛型和子类型化
类、类型和子类型
类和类型的区别
在很多情况下, 类都可以大体上当作类型, 但实际上, 类和类型不是一个东西就比如: 空类型和非空类型, Int
和 Int?
, 请确认下 Int?
是类么?? 不是 那Int?
是类型么? 明显,是类型
又或者: 类 List
而泛型的 List<T>
中 他的类型却很多, 比如: List<Int>
List<Double>
List<Long>
等, 这些都是类型, 而List
是类
子类型关系
子类型说的是一种 关系 , 这种关系在 java 的类, java 的数组里存在, 而在 java 的泛型里却不见了
(书本上的内容, 看不懂看下面)任何时候如果需要的是类型 A 的值,你都能够使用类型 B 的值(当作 A 的值) , 类型 B 就称为类型 A 的子类型。
说简单点, A类指针(java叫引用)指向B类对象, 那么就可以说 A 的子类型是 B, 就这么简单(
val a: A = B()
)
比如: 现在有个引用 val a: Number
和一个Int
类型的对象10
, 如果引用能够直接指向对象val a: Number = 10
则可以说 Int
是 Number
的子类型, 而同时我们可以说 Number
是 Int
的超类型
简单点: 子类的超类型是父类, 父类的子类型是子类, 只要记住这种关系就好
协变和逆变
高端的概念总会有落地的实现, 我们学习要达到的程度是用最简单的一句话描述这些概念
协变(covariant)
- 是什么?
协变: 是一种关系, 一种父类引用 指向 子类对象 的恒定关系
-
Number
引用总能够指向Int
对象, 那么Number
的子类型是Int
, 则Number
和Int
是协变的 -
那么同样的
Number[]
引用 总能够 指向Int[]
, 则Number[]
和Int[]
是协变的 -
同样的,
List<Number>
的引用总能够 指向List<Int>
那么我们也能够说:List<Number>
和List<Int>
有协变关系
但, java 因为历史关系, 使用了类型擦除技术, 所以 任何类型变到泛型的话, 就不会有所谓的协变(逆变)关系, 因为到了运行时期 java 总把类型变成 List<Object>
或者 直接是 List
类型, 如果强制开出协变关系, 则会出现一些安全问题
如果 java 泛型支持协变存在的问题
如果泛型支持协变的话, 会导致下面的问题:
Integer[] a = new Integer[2];
a[0] = 1000;
Object[] o = a;
o[1] = 'a'; // 这里会报错 java.lang.ArrayStoreException: java.lang.Character
复制代码
数组支持协变, 但运行期间不会类型擦除会被检测到类型不同, Integer 引用想指向没有子类型关系的 Character对象, 直接报错
如果把上面的数组完全换成集合就会变成如下代码: (下面这段代码在正常情况下会报错, 但在 满足协变和类型擦除的情况下不会报错)
List<Integer> list = new ArrayList();
list.add(1000);
List<Object> objList = list; // 父类引用指向子类对象, 按理来说 没错 object --> Integer(但实际上这里不会编译通过的)
objeList.add(10.9); // 这里在运行期间将会编译通过, 运行通过, 因为还是 父类引用指向子类的对象, object --> double
复制代码
上面这段集合中, 如果集合同时满足协变和类型擦除的话, 就会编译通过, 运行也将不会报错, 因为在运行期间, 类型类彻底擦除成 Object
这是不允许的, 所以它不支持协变
为了解决上面的问题, java 引入了属于 java 的泛型的协变
java泛型对于"消失的协变关系"的解决方案
协变关系, 又有人叫 子类型关系
java 引入了 通配符?
, 然后用 List<? extends Number>
表示协变, 相当于没有类型擦除的 List<Number>
, 接受Number
及Number
的子类存入List<Number>
集合中
所以 List<? extends Number>
集合可以存入
上面这些类的对象
那么他是如何解决的上面那个问题的呢?
答: java 的解决方法很简单, 一刀切, 如果类型是 <? extends Number>
协变的, 那么他就不允许写入, 修改等操作. 只允许读取
我特么, 解决不了问题, 就解决提出问题的人是吧???
小总结:
List<? extends Number>
不好记里面可以存放什么类, 可以直接认为是 支持协变的List<Number>
理解就好了, 支持协变的话,Number
集合可以存入它和它的子类, 这种情况有人称之为 上界, 因为它限制了类族的上限
对应于 kotlin 的协变关系
在 kotlin 中, 协变将会是: 1. 在类处类型参数协变 2. 在函数处集合泛型的协变
类处类型参数的协变
interface Producer<out T> {
fun produce() : T
}
复制代码
out
放在那里的位置, 主要有两个功能:
- 子类型将会被保留(
Producer<Cat>
是Producer<Animal>
的子类) T
只能用在out
位置
in
的位置在函数参数,out
位置在函数返回值, 既是in
又是out
则不需要标记, 同样的out
标记的泛型只能读取, 不能写入,in
标记的泛型只能写入不能读取(和java优点不太一样???)
-
构造函数的参数使用
T
为泛型既不在in
也不在out
, 但如果参数前面加上了val/var
则需要注意in/out
了, 因为 它不再是参数而是属性, 属性有setter/getter
-
MutableList
不能使用out
-
协变后的集合不允许写入, 只允许读取
-
协变的
out B
表示只能填入B
或者B的子类
函数处集合泛型的协变
和 java 类似的用法
out T
对应了 java 的 ? extends T
in T
对应了 java 的 ? super T
private fun f(list: ArrayList<out B>) {
// 使用时不允许写入
// list.add(D()) // 参数变成 Nothing 了, 所以不能添加 D() 对象
list.forEach {
println(it.javaClass)
}
}
复制代码
逆变(contravariant): 相反的子类关系
正常情况下, Animal
是 Cat
的父类, Animal
的子类型是 Cat
, List<Animal>
也是List<Cat>
的子类型, 这是协变, 但如果 List<Cat>
是List<Animal>
的子类型的话, 这种子类型关系逆反了, 这就是逆变
kotlin 中的逆变
同样的 kotlin 支持: 1. 类的泛型参数逆变 2. 函数集合参数泛型逆变
类的泛型参数逆变
class A<in T> {
fun write(t: T) {
}
}
复制代码
函数集合参数泛型逆变
open class A
open class B : A()
open class C : B()
open class D : C()
fun c(list: ArrayList<in C>) {
// list.add(A()) // 泛型使用时, 可以直接当成 ArrayList<C>
// list.add(B()) // 泛型使用时, 可以直接当成 ArrayList<C>
list.add(C()) // 符合父类引用指向子类对象
list.add(D()) // 符合父类引用指向子类对象
list.forEach {
println(it::class.java)
}
}
fun main() {
val l1: ArrayList<A> = arrayListOf(A(), B())
val l2: ArrayList<D> = arrayListOf(D())
// val l3: ArrayList<in C> = arrayListOf<D>(D()) // 报错
val l4: ArrayList<in C> =
arrayListOf(D()) // arrayListOf(D()) 是 ArrayList<C> 类型, kotlin编译器做了恶心的优化, 提高了泛型的类型, 从D变成了C, 所以不会报错
c(l1) // 符合 Any ~ C类型
// c(l2) // 错误, 只能接受 Any 到 C 范围内的泛型, 实际是 ArrayList<in C>(Any ~ C类型) 指向 ArrayList<D>
c(arrayListOf(D())) // 类型是 ArrayList<C>, 所以不会报错
}
复制代码
-
逆变初始化时(实参赋值给形参时
in C
), 代表一个范围的类型[Any, C]
没有具体指向某个类型, 是一个宽泛的类型, 这是逆变主要存在的作用 -
逆变使用时, 把它当作
ArrayList<C>
就好了. 把要写入的元素限定在C
的子类, 限定C
函数也会多保留些, 也不会很多争议说为什么不限定为Any
还是A
又或者是B, C
防止出现限定为
B
, 写入为A
的存在, 一刀切, 这样就为满足赋值兼容性原则(父类引用指向子类对象)
- 逆变后的集合只能写入, 不能读取, 读取的话类型会消失, 他会返回
[Any, C]
中的任何一个可能的类型, 所以直接返回了Any
, 类型消失了
至于为什么我要说要满足赋值兼容性原则原则? 我想说, 逆变说的好听叫逆子类型关系, 其实说到底是一个 保证一种类型在一个范围里面罢了, 比如:
fun c(list: ArrayList<in C>) {
list.add(C())
list.add(D())
list.forEach {
println(it::class.java)
}
}
fun main() {
val l: ArrayList<A> = arrayListOf(A())
c(l)
}
复制代码
看起来是 C -> A
, 子类引用指向了父类对象, 其实何尝不是[Any, C] -> A
这样的呢? 一个类型集合 -> A类
, C
不行, Any -> A
总行了吧? 还是符合赋值兼容性原则
使用协变和逆变写个 copyData
函数
- 普通方式实现该函数
fun <T> copyData01(source: MutableList<T>, destination: MutableList<T>) {
for (item in source) {
destination.add(item)
}
}
复制代码
- 使用约束的方式实现该函数
/**
* T 是 R 的子类或者 T 就是 R, 记作: T <= R
* 所以 source: MutableList<T> 是子类集
* destination: MutableList<R> 是父类集
* 把子类集source的 item 依次给 父类集的 destination
*/
fun <T : R, R> copyData02(source: MutableList<T>, destination: MutableList<R>) {
for (item in source) {
destination.add(item)
}
}
复制代码
- 使用协变的方式实现函数
/**
* 对读取函数使用 out 泛型修饰符
* out T 表示 T 或者 T 的子类
*/
fun <T> copyData03(source: MutableList<out T>, destination: MutableList<T>) {
for (item in source) {
destination.add(item)
}
}
/**
* in T: T 的父类
*/
fun <T> copyData04(source: MutableList<T>, destination: MutableList<in T>) {
for (item in source) {
destination.add(item)
}
}
/**
* 下面这就是声明处变型
*/
fun <T> copyData05(source: MutableList<out T>, destination: MutableList<in T>) {
for (item in source) {
destination.add(item)
}
}
/**
* List 本身就是只读的, 所以看 List 源码的话会看到 public interface List<out E> 这段代码
* 看到 out E 了么?
*/
fun <T> copyData06(source: List<T>, destination: MutableList<in T>) {
for (item in source) {
destination.add(item)
}
}
复制代码
在 kotlin 中, 如果泛型被标记为
out
, 则该泛型只能调用符合泛型out
位置的函数, 比如fun get() : T
, 如果泛型被标记为in
, 那么只能调用该类的复合in
位置的函数比如:fun add(t: T): void
out
协变, 只读,in
逆变, 能读写, 但写会失去类型, 没有in/out
不变
泛型类中 out
和 in
的位置
kotlin 支持在 类声明 处定义泛型的 变型 , 也支持像 java 一样在 函数位置写上 变型
星号投影: 使用 *
代替类型参数
- 星号投影不清楚存入的类型到底是哪个, 所以一般不做写入, 仅作读取
所以功能上类似于 List<out Any?>
, 在没有任何类型信息的情况下, Any
是最好的选择
- 使用星号投影的, 说明开发者并不需要知道读取出来的泛型具体是什么类型
说白一点, 星号投影把它当作 out Any? 吧, 读取出来的对象当作 Any? 对象就行, 不能写入