Kotlin泛型

Kotlin 泛型

类型擦除

kotlin伪泛型

与Java一样,kotlin的泛型也是编译时期的泛型,在运行时期的时候泛型会被擦除掉,这种泛型可以理解为伪泛型,与之相对,真泛型即在运行期还存在泛型类型。可以通过以下两个例子来验证kotlin类型擦除:

1
2
3
4
5
6
7
8
9
10
// demo 1
fun main(){
val a : List<String> = ArrayList<String>()
val b : List<Int> = ArrayList<Int>()
println(a.javaClass == b.javaClass)
}
************************************************
运行结果:
true
Process finished with exit code 0

上面demo 1代码中定义了两个泛型变量,分别是ArrayList<String>ArrayList<Int>,并通过获取javaClass对比,结果相等,说明在运行时泛型被擦除了,只剩下了原始类型

1
2
3
4
5
6
7
8
9
10
11
// demo 2
fun main(){
val c : MutableList<Int> = mutableListOf(1,2,3)
val method : Method = c::class.java.getMethod("add",Any::class.java)
method.invoke(c,"list")
println(c)
}
*********************************************
运行结果:
[1, 2, 3, list]
Process finished with exit code 0

demo 2中定义了一个泛型类为MutableList<Int>的变量c,变量c的泛型为<Int>,但是通过反射能够添加<String>类型的值,说明运行过程中泛型被擦除,没有了泛型约束

通过代码反编译也很容易观察到类型擦除:

1
2
3
4
5
6
7
8
9
10
11
// Kotlin 代码
fun <T> testGenericOne(): T?{
val t : T? = null
return t
}
// 反编译后的Java代码
@Nullable
public static final Object testGenericOne() {
Object t = null;
return t;
}

从反编译的代码中可以观察到泛型<T>被擦除掉了,在实际运行过程中的类型是Object

reified inline

上述的代码可以证实Java(kotlin)确实存在类型擦除,那么为什么会存在类型擦除呢,这是因为在Java1.5版本之前没有泛型,泛型是在Java1.5版本引入的,因而为了兼容1.5版本及1.5之前的版本,Java选择伪泛型,否则Java需要就需要修改整个底层

为了版本兼容选择类型擦除无可厚非,但是类型擦除同时也带来了一些限制:

1
2
3
fun <T> testGeneric(t: T){
println(T::class.java)
}

这段代码无法编译通过,泛型T只是一个泛型参数,无法获取Class信息,同样这也是伪泛型的体现;因为无法通过泛型参数获取Class,因此有时候需要另外传递一个Class参数,比如Gson框架中的fromJson方法:

1
2
3
public <T> T fromJson(String json, Class<T> classOfT) throws JsonSyntaxException { 
return gson.fromJson(json, clazz);
}

传递额外参数可以满足需要类对象的场景,kotlin还有另外更彻底的实现方式,通过关键字reifiedinline的结合使用,把函数定义成内联函数,同时可以将函数中的泛型参数当成真实参数使用:

1
2
3
4
5
6
7
8
// reified inline 使用
inline fun <reified T> testGenericTwo(){
println(T::class.java)
}
********************************************
运行结果:
class java.lang.String
Process finished with exit code 0

Kotlin型变

Java协变和逆变

假设有两个类FatherSonSonFather的子类,但是包含这两个泛型的泛型类之间没有任何关系,例如:List<Father>List<Son>之间没有任何关系,这也意味着以下的代码是走不通的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class Father{}
class Son extends Father{}
class Family<T>{}
void addElement(Family<Father> family){}
public void main(){
Family<Son> sonFamily = new Family<>();
addElement(sonFamily);
}
************************************************
编译报错:
Type mismatch.
Required:
Family<Father>
Found:
Family<Son>

这段代码提示类型不匹配的,Family<Son>类型对象并不能作为Family<Father>类型参数传递。即使SonFather之间存在继承关系,但是Family<Son>Family<Father>之间并没有半毛钱关系;可是在实际开发中有时候会有这样的需求,希望带泛型类型的参数也能够实现多态传递,这个时候就需要借助通配符?,使用? extends? super ? extends是上界通配符,能够使Java泛型具有协变性,? super是下界通配符,能够使Java泛型就有逆变性。

在继承关系树中,子类继承自父类,可以认为父类在上,子类在下。extends 限制了泛型类型的父类型,所以叫上界。

与上界通配符对应,这里 super 限制了通配符 ? 的子类型,所以称之为下界。

1
2
3
4
5
6
7
8
9
10
11
12
class Father{}
class Son extends Father{}
static class Family<T>{}
public static void addChildElement(Family<? extends Father> family){}
public static void addParentElement(Family<? super Son> family){}

public static void main(String[] args){
Family<Son> sonFamily = new Family<>();
Family<Father> fatherFamily = new Family<>();
addChildElement(sonFamily); // 协变调用
addParentElement(fatherFamily); // 逆变调用
}

通过使用? extends,允许传入参数可以是泛型类型参数Father的子类型的任意类型,这种用法就是协变;与之相反,使用? super,就是允许传入参数可以是泛型类型参数Son的父类型的任意类型,这个用法就是逆变。

与Java相似,Java通过? extends? super来实现Java泛型的协变和逆变,Kotlin则通过泛型关键字outin关键字来定义协变和逆变;或者可以说out等同于? extendsin等同于? super

协变

out:泛型关键字,实现kotlin泛型协变,out关键字的使用可以是在泛型类定义中,也可以是在带泛型类型参数的函数中

out关键字的使用
1
2
3
4
5
6
7
8
open class Father{}
class Son : Father(){}
class Family<out T>{}
fun addElement(element: Family<Father>){}
fun main(){
val sons : Family<Son> = Family()
addElement(sons)
}

对于协变来说,假设类型A和类型B存在继承关系,且类型A是类型B的子类,那么通过协变Family<A>Family<B>的子类型,这个是协变的一个特点

另外一个角度理解协变

通过Java的型变分析,泛型型变的出现是为了使得泛型使用更加的灵活,kotlin也是一样;但是同时也需要对型变做一些限制,对于协变来说,规定类中的参数被声明为协变,那么就限制这个参数在该类中的使用:out关键字声明的泛型参数只能出现在out位置,out位置如下图:

kotin泛型"out"位置

“out”位置是声明函数返回值类型的位置,其主要是函数运行结束时产生指定的泛型对象,其扮演的是生产者的角色,所以用“out”声明很形象。那么,声明为协变的泛型为什么一定要限制在out位置呢?这是出于泛型安全考虑:假设有垃圾桶,垃圾桶分别用来装可回收垃圾、有害垃圾

1
2
3
4
5
6
7
8
9
10
11
open class Waste
class recyclableWaste : Waste()
class HarmfulWaste : Waste()
abstract class Ashbin<out T>{
// demo 实现垃圾分类例子 设置为抽象类,抽象方法

// 倒垃圾
abstract fun pour() : T
// 扔垃圾到垃圾桶 这个方法是错误的 假设成立
abstract fun put(t : T)
}

RecyclableWaste类是Waste类的子类,由于泛型类Ashbin<out T>用关键字out声明泛型参数为协变,泛型类Ashbin<out T>子类型化得以保留;

1
2
3
4
5
6
7
8
9
10
11
12
13
14
/**
* 投放垃圾
*/
fun wast(ashbin: Ashbin<Waste>){
// 有害垃圾
val waste : Waste = HarmfulWaste()
ashbin.put(waste)
}

fun main(){
// 声明一个可回收垃圾桶
val ashbin: Ashbin<RecyclableWaste> = Ashbin<RecyclableWaste>()
wast(ashbin)
}

假设泛型类Ashbin<out T>中的put(t : T)函数成立,那么根据以上代码,就会出现这样一种情况:main() 函数中声明了一个用来装可回收垃圾的垃圾桶,并将其作为参数传递给wast(ashbin: Ashbin<Waste>)函数,但是却在该函数中投放如有害垃圾;显而易见,在可回收垃圾桶中投放有害垃圾是一种错误的做法,不符合垃圾分类的原则。由此可见,在声明为协变的泛型类中,泛型出现在了函数参数的位置上会导致泛型安全问题,这是限制协变声明的泛型类的泛型参数只能出现在"out"位置原因,这一限制在Java泛型中同样也适用:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
static class Waster{}
static class RecyclableWaste extends Waster{}
public static class Ashbin<T>{
T t;
public T get(){
return t;
}

public void set(T t){
}
}

public static void main(String[] args){
Ashbin<? extends Waste> ashbin = new Ashbin<>();
RecyclableWaste recyclableWaste = new RecyclableWaste();
// 编译报错
ashbin.set(recyclableWaste);
}

协变泛型参数消费使用报错(Java).png

协变总结

对泛型类泛型参数协变,即用out关键字声明,那么:

  • 子类型化会被保留(Family 是Family的子类型)
  • 泛型类型(T)必须只能用在"out"位置上

逆变

逆变与协变不同,甚至可以说是相反,主要体现在子类型化关系上。假设有子类Dog和父类Animal,通过逆变,则Zoom<Animal>Zoom<Dog>的子类型;可以看出不同类型的关系和声明逆变后泛型类之间的关系是截然相反的,这也是逆变的特点。

逆变是通过关键字in来声明的:

1
2
3
4
5
6
7
8
9
10
11
open class Animal
class Dog : Animal()
class Cat : Animal()
class Zoom<in T>
fun addAnimal(animal: Zoom<Dog>){}

fun main(){
// in
val animal : Zoom<Animal> = Zoom()
addAnimal(animal)
}

和协变一样,对于逆变的使用同样也需要限制:对声明为逆变的泛型类,其泛型类型参数在泛型类内部只能被消费,而不能作为生产者,即泛型参数只能出现在in位置而不能出现在out位置,如下图:

逆变in位置.png

同样,可以使用垃圾和垃圾桶的关系来解释下为什么逆变的泛型参数只能被放置在"in"位置:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
open class Waste
class RecyclableWaste : Waste()
class HarmfulWaste : Waste()
class RefuseBin<in T>{
// 倒垃圾 泛型参数T 不能作为返回类型 编译错误
fun pour() : T{
}
// 扔垃圾到垃圾桶
fun put(t : T){}
}
fun putWaste(refuseBin: RefuseBin<RecyclableWaste>){
val recyclableWaste : RecyclableWaste = RecyclableWaste()
refuseBin.put(recyclableWaste)
}
fun main(){
// 逆变调用
val refuseBin : RefuseBin<Waste> = RefuseBin()
putWaste(refuseBin)
}

对于逆变,泛型参数作为消费使用的时候,传入的参数一定是该泛型T的子类,能够保证泛型使用的安全;上述代码中定义了一个函数putWaste,需要传递的参数是RefuseBin<RecyclableWaste>,在函数中声明一个可回收垃圾,然后放入垃圾桶中,从逻辑上看,可回收垃圾桶放入可回收垃圾,没毛病;由于泛型参数被声明逆变,RefuseBin<Waste> RefuseBin<RecyclableWaste>的子类型,那么在main函数中传递给 putWaste的参数是RefuseBin<Waste>是合理的,RefuseBin<Waste>是一个可以装任意垃圾的垃圾桶,垃圾桶装可回收垃圾,也没有毛病,666。因此泛型参数用在in位置是合法的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
open class Waste
class RecyclableWaste : Waste()
class HarmfulWaste : Waste()
class RefuseBin<in T>{
// 倒垃圾 泛型参数T 不能作为返回类型 编译错误
fun pour() : T{
}
// 扔垃圾到垃圾桶
fun put(t : T){}
}
// 倒出可回收垃圾
fun pourRecyclableWaste(refuseBin: RefuseBin<RecyclableWaste>) : RecyclableWaste{
val recyclableWaste : RecyclableWaste = refuseBin.pour()
return recyclableWaste
}
fun main(){
// 逆变调用
val pourWaste : RefuseBin<Waste> = RefuseBin()
pourRecyclableWaste(pourWaste)
}

假设在逆变声明的RefuseBin<in T>中的pour函数是合法的,定义两个函数:pourRecyclableWaste函数中传递的参数是RefuseBin<RecyclableWaste>,在pourRecyclableWaste函数中返回从可回收垃圾桶中获取的可回收垃圾,但是在main函数中传递给pourRecyclableWaste的是可以装任意垃圾的垃圾桶(RefuseBin<Waste>),这样就会导致在pourRecyclableWaste获取到的就有可能不是可回收垃圾,因为普通垃圾桶可能倒出有害垃圾或者其他垃圾,这就出现了泛型类安全问题。所以逆变泛型类中泛型参数不能作为生产者角色出现(即不能出现在out位置)。

逆变总结

对泛型类泛型参数逆变,即用in关键字声明,那么:

  • 子类型化关系会被逆转(Family 是Family的子类型)
  • 泛型类型(T)必须只能用在"out"位置上

不变和点变形

不变,顾名思义就是既不协变也不逆变,就是普通的泛型类,因此泛型参数即可以放在in位置也可以用到out位置,比如kotlin自带的MutableList<E>

1
2
3
4
5
6
7
8
9
10
11
12
public interface MutableList<E> : List<E>, MutableCollection<E> {
override fun add(element: E): Boolean
override fun remove(element: E): Boolean
override fun addAll(elements: Collection<E>): Boolean
public fun addAll(index: Int, elements: Collection<E>): Boolean
override fun removeAll(elements: Collection<E>): Boolean
override fun retainAll(elements: Collection<E>): Boolean
override fun clear(): Unit
public operator fun set(index: Int, element: E): E
public fun add(index: Int, element: E): Unit
public fun removeAt(index: Int): E
}

那么,问题来了,泛型不变是能够使泛型参数同时出现在in位置也可以出现在out位置,但是却失去了协变和逆变的特性,代码的复用性就很差,这不是一朝回到解放前么;能不能同时保留协变和逆变呢?是可以的,通过点变形来实现,如下代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
open class Waste
class RecyclableWaste : Waste()
class HarmfulWaste : Waste()
class TrashCan<T>{
// 倒垃圾
fun pour() : T{}
// 扔垃圾到垃圾桶
fun put(t : T){}
}
fun putWasteWithIn(trashCan : TrashCan<in RecyclableWaste>){
val recyclableWaste : RecyclableWaste = RecyclableWaste()
trashCan.put(recyclableWaste)
}

fun pourWasteWithOut(trashCan : TrashCan<out RecyclableWaste>): RecyclableWaste{
val recyclableWaste : RecyclableWaste = trashCan.pour()
return recyclableWaste
}

如上所示,我们可以把泛型类声明为不型变的,但是在使用它的时候,加上out或者in,让它在使用的时候产生型变,通过这种方式,代码更加灵活,满足了既需要泛型参数作为函数参数类型又需要泛型参数作为函数返回值类型的类。但是同时也存在限制:如上代码,在pourWasteWithOut函数中不能够调用TrashCan<T>类的put函数;在putWasteWithIn函数中不能够调用TrashCan<T>类中的pour函数;具体原因如同上面的协变逆变分析。

星型投影 * 和 泛型边界

星型投影*

星型投影,其实就是:假如你对于你现在泛型要传入的泛型参数不确定或者是无所谓的时候,为了确保泛型类能够正确的编译运行,可以借助星型投影*,而且该泛型类型的每个具体实例化将是该投影的子类型,kotlin中星型投影*的使用类似于java中的使用:

1
2
3
4
5
6
public static void main(String[] args){
// java通配符?使用
ArrayList<?> array;
array = new ArrayList<String>();
array = new ArrayList<Integer>();
}

对应kotlin的代码:

1
2
3
4
5
6
// kotlin星型投影
fun main(){
var list : ArrayList<*>
list = ArrayList<String>()
list = ArrayList<Int>()
}

Kotlin 可以根据 * 所指代的泛型参数进行相应的映射,下面是官方的说法:

  • 对于 Foo <out T : TUpper>,其中 T 是一个具有上界 TUpper 的协变类型参数,Foo <*> 等价于 Foo <out TUpper>。 这意味着当 T 未知时,你可以安全地从 Foo <*> 读取 TUpper 的值。
  • 对于 Foo <in T>,其中 T 是一个逆变类型参数,Foo <*> 等价于 Foo <in Nothing>。 这意味着当 T 未知时,没有什么可以以安全的方式写入 Foo <*>
  • 对于 Foo <T : TUpper>,其中 T 是一个具有上界 TUpper 的不型变类型参数,Foo<*> 对于读取值时等价于 Foo<out TUpper> 而对于写值时等价于 Foo<in Nothing>

如果泛型类型具有多个类型参数,则每个类型参数都可以单独投影。
例如,如果类型被声明为 interface Function <in T, out U>,我们可以想象以下星投影:

  • Function<*, String> 表示 Function<in Nothing, String>
  • Function<Int, *> 表示 Function<Int, out Any?>
  • Function<*, *> 表示 Function<in Nothing, out Any?>

泛型边界

在 Java 中,我们可以用 extends 关键字为泛型参数指定上限,如下:

1
2
3
// java 
interface Bound{}
interface GenericBound<T extends Bound>{...}

而对应的kotlin写法类似:

1
2
interface Bound{}
interface GenericBound<T : Bound>{...}

如果同一类型参数需要多个上界,在Java中的写法是这样的:

1
2
3
interface Bound{}
interface Generic{}
class GenericBound<T extends Bound, Generic>{...}

在kotlin中同一类型参数需要多个上界与Java的写法有很大区别,kotlin的写法中多了关键字where,如下:

1
2
3
interface Bound{}
interface Generic{}
class GenericBound<T> where T : Bound , T : Generic{}

总结

kotlin泛型中比较难理解的是协变和逆变,感觉学习起来会比较的抽象,但是协变和逆变在Java中就有所体现,只不过是在kotlin中的使用方式与Java不同,kotlin中使用变得更加的简便;同样对于泛型边界来说,虽然多了一个关键字where,但是内容确实与Java完全一致,没有新增任何东西;总的来说,kotlin泛型的内容是与Java泛型一致的,只是在写法上有区分而已,要学习kotlin的泛型,可以先将Java泛型完全掌握,只要掌握了Java泛型,kotlin泛型学习起来就十分的简单,无非就是换个写法而已。