深入kotlin - 范型
Posted 颐和园
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了深入kotlin - 范型相关的知识,希望对你有一定的参考价值。
范型
定义
class A<T>(t:T)
var a: T
init
this.a = t
......
var aa = A("kotlin")
println(aa.a) // 打印 kotlin
其中 A<String>("kotlin")
可以根据类型推断简写写成 A("kotlin")
。
协变、逆变
所谓协变、逆变是指一个对象被意外地降级为子类/升级为父类。特别是集合操作中,很容易出现此类问题。
在 Java 中,List<String>
并不是 List<Object>
的子类,因此不能把 List<String>
赋值给 List<Object>
。
但是对于数组来说则不同,因为数组天然支持协变。看如下代码:
Object[] arr = new String[]"aaa","bbb";
arr[0] = new Object();
第一句由于数组天然支持协变,所以 String 数组属于 Object 数组子类,因此把一个 String 数组赋给 Object 数组是可行的。
但对于第二句,arr[0] 实际上指向的是一个 String,将一个 Object 赋值给他,实际上是将父类赋给了子类——在面向对象中这是绝对不行的。这实际上会导致一个运行时异常。但 Java 在编译时却不能检查出来,在编译器看来,Object 数组中的元素自然也是 Object,将一个 Object 对象赋值给他自然是行得通的。但是在实际使用这个数组的过程中,由于数组这种允许协变的特性,有可能会导致它的元素被意外降级为子类(比如将一个 String 数组赋值给它,那么它所有的元素实际上都被降级为 String ),因此协变的出现对 Java 来说是一个威胁。
为了解决此问题,Java 提供了 ?通配符:
List<? extends Object> list // 表示元素类型包括 Oject 及其子类。
因此List<String>
是 List<? Extends Object>
的子类,而非List<Object>
的子类。List<? Extends Object>
这样的范型限制了类型的上界。当我们向 List 中放入一个 String 元素后再读去这个元素,这个元素缺只能被当成 Object 看待,这种情况称之为协变。
与此相反,是逆变的概念:
List<? super String> // 其元素类型只能是 String 及其父类
当你向这个 List 中写入对象时,不仅仅可以放入 String 对象,还可以放入 String 的父类对象,比如 Object。
Java 通过在读取时使用<? extends E> ,在写入时使用<? super E>来实现协变和逆变。
总结一下,所谓协变逆变,都是范型约束的一种泛化,协变是向下(到子类)的泛化,逆变是向上(到父类)的泛化。
如果一个范型被指定为允许协变,表示这个范型可以和子类兼容,反之,逆变则允许和父类兼容。
在Kotlin 中,用 out 关键字来解决协变的问题:
class A<out T>(t: T)
private var t: T
init
this.t = t
fun get(): T = this.t
这里, 表示该范型仅仅用于读取(get 方法),因此它将允许向下协变,即可以涵盖 T 及其子类。比如:
var aa:A<Any> = A<String>("abc") // 虽然传入的是 String,但是可以当成 Any,当然读出来的时候已经变成 Any 而不是 String
虽然 aa 被定义为 A 类型,但是我们把一个 A 赋值给它也是没问题的,因为 String 是 Any 的子类。
类似地,in 关键字用于解决逆变问题:
class B<in T>(t: T)
private var t: T
init
this.t = t
fun set(t: T)
this.t = t
表示该范型仅仅用于写入(set 方法),因此它允许向上逆变,即可以涵盖 T 及其父类:
var aa:A<Int> = A<Number>(3) // 虽然传入的是 Number, 但是可以当成 Int
虽然 aa 被定义为 A 类型,但是我们把一个 A 赋值给它,因为 Number 是 Int 的父类。
协变只能用于读,逆变只能用于写
请看如下 java 代码:
List<Cat> cats = new ArrayList<>();
cats.add(new Cat());
List<? extends Animal> animals = cats;
Animal animal = animals.get(0); // 允许读取到 Animal
System.out.println(animal); // 打印出来的实际是 com.Cat@66d3c617,但你仍然无法赋值给 Cat 对象
animals.add(new Animal());
这时编译器在最后一句报错: capture<? extends com.Animal> cannot be applied to com.Cat
这说明,协变只允许读取,不允许写入(比如 add)。
这其中的道理很容易理解,因为 animals 定义为List<? extends Animal> 它可以在运行时被赋值为 List、List…因为它们都是List<? extends Animal> 的子类。 如果像代码中一样,animals 被一个 List 赋值,那么它里面的元素就应该是 Cat 对象,如果 java 允许对元素进行赋值,那么很可能会错误地把一个 Animal 对象赋给一个 Cat 元素,这在 Java 中是绝对不允许的(在 java 中,将父类赋值给子类对象是不允许的,反之可以)。为了防止这类错误发生,编译器直接禁止了对协变的类型进行 set 操作。
逆变则相反:
List<Animal> animals = new ArrayList<>();
List<? super Cat> cats = animals;
cat.add(new Cat());
Cat cat = cats.get(0);
此时编译器报错: Required: com.Cat, Found: capture<? super com.Cat>,说明逆变不允许读取,但可以写入(比如 add)。
我们打印 get(0) 得到的对象类型,确实就是 Cat:
System.out.println(cats.get(0)); // 打印:com.study.Cat@66d3c617
但编译器不允许对 cats 进行读取——准确地说,不允许读取到一个 T 类型的变量中,如果读取到 Object 对象是没问题的:
Object cat = cats.get(0);
这其中的道理不难理解,因为 cats 的定义是List<? super Cat> ——集合中的元素可能是 Cat 及其父类类型,那么当你从中读取一个元素再赋给一个 Cat 对象时,读取的元素很可能是一个 Animal,在 java 中,将父类赋值给子类对象是不允许的(反之可以)。
out 允许范型降级
Kotlin 通过 out 关键字允许协变,或范型降级(即声明范型时是某个类型,但通过某些手段可以将它降级为声明时类型的子类):
class A<out T>(private val value: T) // 1
... ...
val aa = A("out sample") // 2
val aaa: A<Any> = aa // 3
- class A 有一个允许协变的范型 T(用 in 关键字),因此这个 T 允许降级,即可以将子类赋值给父类。
- aa 是一个 A 类型(通过类型推断)。
- aaa 是一个 A 类型,由于允许协变,虽然 T 为 Any,但我们可以将 Any 的子类 String 赋值给 T。这样,实际上 T 从 Any 被降级为 String 了。因为这中操作限制了写入操作,你实际上不可能将 value 读取到一个 String 变量中了(编译器报错)。
out 用作返回类型
如果一个范型被定义为 out ,那么这个范型可以作为方法返回类型,但不能作为方法的参数类型,这点和 out 的字面意义是一致的。反过来,如果你发现一个范型仅仅是充当方法的返回类型使用,那么我们可以将该范型定义为 out。
in 允许范型升级
与此相反,in 关键字允许逆变,即将范型升级为父类。
class B<in T> // 1
fun toString(value: T): String
return value.toString() // 2
... ...
val bb = B<Number>() // 3
val bbb: B<Int> = bb // 4
assertTrue(bbb is B<Int>) // 5
- class B 有一个允许逆变的范型 T(范型升级),使用 in 关键字。
- 逆变只能用于写,不能用于读,因此直接返回 value 是不行的(因为它是 T 类型),但返回 value 的 toString 方法是可以的(它不再是 T 类型,而是 String 类型)。
- bb 是一个 B 类型。
- 逆变允许升级,虽然 bbb 在声明时指定 T 为 Int,但是可以将 Int 的父类 Number 赋值给 T,实际上 T 是被升级了。
- 虽然 T 被升级,但它仍然还是定义时的 Int。因此断言通过。
in 用作参数类型
如果一个范型被定义为 in ,那么这个范型可以作为方法参数类型,但不能作为方法的返回类型,这点和 in 的字面意义是一致的。反过来,如果你发现一个范型仅仅是充当方法的参数类型使用,那么我们可以将该范型定义为 in。
范型不能同时修饰 out 和 in
同时,不能在同一个范型上既使用 out 又使用 in。如果一个范型既要作为方法的参数类型,又要作为方法的返回类型,那么这个范型不使用任何关键字修饰(这被称之为不变
)。
out 和 in 本质上是多态
多态中规定,子类实例可以赋值给一个父类的引用。
这在于协变很好理解——因为它本就是范型降级,逆变则比较难于理解了,让我们举一个逆变的例子说明:
Interface Consumer<in T>
fun consumer(item: T)
Consumer 接口定义了一个范型 in T,表示这是一个逆变范型,即允许声明 T 时用子类声明,但使用时指定一个父类。同时,在方法中使用了范型 T。
然后定义两个类:
class Human:Consumer<Fruit>
override fun consume(item: Fruit)
println("Consume Fruit")
class Man: Consumer<Apple>
override fun consumer(item:Apple)
println("Consume Apple")
Human 和 Man 之间没有继承关系。Human ''消费" 的是 Fruit,Man “消费” 的是 Apple。这里的消费即指方法所接收的参数。则我们可以这样做:
val consumer1: Consumer<Fruit> = Human()
val consumer2: Consumer<Apple> = Human()
consumer1 声明的时候 T 是 Fruit,使用的时候是也是 Fruit(查看 Human 的定义),声明和使用一致,即所谓的不变。这是没有任何问题的。
consumer2 声明的时候 T 是 Apple,但使用的时候却是 Fruit(Fruit 是 Apple 的父类),T 在使用时中被升级了,这就是所谓的逆变。多态中规定,子类实例可以赋值给父类引用。但这里明明是将一个父类赋值给了子类,怎么解释?
但是本质上这仍然是多态,consumer2 将 T 声明为 Apple,则你在调用 consume 方法时必然只能传递一个 Apple 参数给它(否则编译器不通过):
consumer2.consume(new Apple())
但是 consumer2 实际上是一个 Human,因此调用的是 Human 的 consume(item: Fruit) 方法。那么相当于是把一个 Apple 对象传给了 Fruit 参数,这不正是把子类实例(Apple)赋值给父类引用(Fruit)吗?符合多态的定义。
使用处协变(类型投影)
前面提到 kotlin 是声明处协变,Java 是使用处协变。但实际上 kotlin 也支持使用处协变(又称类型投影)。声明处协变的限制在于它实际上对范型进行了约束,对于声明为 out 的范型,你在使用时就只能作为方法返回值而不能作为参数,这一点是不现实的。
以 Array 为例,Array 声明时即没有使用 out 也没有使用 in 修饰,这是因为 T 既在返回类型时使用(get 方法),也在参数中使用(set方法)。
在这种情况下,Array 有可能产生使用处协变。
fun copy(from: Array<Any>, to: Array<Any>)
assertTrue(frome.size == to.size)
for(i in from.indices)
to[i]=from[i]
fun main(args: Array<String>)
val from:Array<Int> = arrayOf(1,3,5,6)
val to:Array<Any> = Array<Any>(4, "hello"+it)
for(item in to)
println(item)// "hello0","hello1","hello2","hello3"
copy(from, to)
在 copy(from, to) 处出现警告, from 的类型不匹配:定义的是 Array 但是提供了一个 Array。虽然 Int 是 Any 的子类,但 Array 并不是 Array 的子类。
我们可以将 copy 方法中的 from 参数重定义为 Array ,这样编译就可以通过了。这就是使用时协变。它和声明时协变的区别在于,它不是在定义该范型的地方使用 out,而是在实例化的地方使用 out:
// 范型声明
Consumer<in T>
// 范型实例化
Array<out Any>
范型声明,是声明某个类型不确定,使用一个占位符来代替这个类型,你可以用任意名字,比如 T 或 S (只要符合明明规范)来暂代这个类型名,这个占位符,我们可以称之为“范型变量”。而范型的实例化,则是指将某个类型赋值范型变量,类似于 T = Any 这样。使用处协变,就是指在类型名前使用 out 关键字,声明处协变,就是指在范型变量名前使用 out 关键字。
因此 out 不仅仅可以修饰范型变量, 也可以修饰某个类型。
此外,使用处协变仍然受到和声明时协变一样的限制,即可以读不能写,比如:
for (i in from.indices)
to[i] = from[i]
from[i] = i // 编译错误
编译器将在 from[i] 处报错,因为 from 中的元素是 类型,你不能向< out Any> 进行写入操作。但是对 to[i] 进行写入操作就没事:
to[i] = from[i]
因为 to 中的元素是 类型,没有 out 的限制。
本质上讲,使用处协变仍然是通过类型约束来保证范型的安全性。
使用处逆变
类型投影还包括另外一种情况,即使用处逆变。
fun setValue(to:Array<String>, index: Int, value:String)
to[index] = value
fun main(args:Array<String>)
val array:Array<Any> = Array<Any>(4, it->"hello"+it)
for(item in array)
println(item) // "hello0","hello1","hello2","hello3"
setValue(array, 1, "world") // 编译错误
此时 setValue(array, 1, “world”) 一句报错,依然是 array 类型不匹配,需要 Array,但提供了 Array。解决的办法是使用使用处逆变,在 setValue 方法第一个参数处使用 in:
fun setValue(to:Array<in String>, index: Int, value: String)
道理和使用处协变是一样的。限制仍然是允许对 to 进行写入操作,但不能读取。
星投影
星投影其实指的是<*>
。在范型协变中,<*>
可以用于表示,在范型逆变 中,<*>
可以用于表示 。Nothing 在 kotlin 中表示这个类型没有任何实例,因此不能向其中写入任何值。
class Star<out T>
class Star2<in T>
fun setValue(t:T)
fun main(args: Array<String>)
var star: Star<Number> = Star<Int>()
var star2: Star<*> = star // 1
var star3: Star2<Int> = Star2<Number>()
var star4: Star2<*> = star3 // 2
star4.setValue(3) // 3 编译错误
- star2 的类型为 Star
<*>
表示范型 T 的类型不确定,但将 star 赋值给它后,T 可以依靠类型推断。因为 val star: Star,T 的上界(Tupper)就是 Number,因此将 star 赋值给 star2 之后,Star<*>
就相当于 Star。 - star4 的类型为 Star2
<*>
表示范型 T 的类型不确定,依靠 star3 的类型推断。因为 var star3: Star2 ,因此 Star2<*>
就相当于 ,类型未知。 - 因为 不能写入任何类型,因此这句报错。但是将 star4 改成 star3 之后,就可以了。
如果对于不变的范型(没有 in 也没有 out 修饰),星投影<*>
又会怎么样呢?看一个例子:
class Star3<T>(private var t:T)
fun setValue(t:T)
fun getValue():T return this.t
fun main(args:Array<String>)
var star5: Star3<String> = Star3<String>("hell0")
var star6: Star3<*> = star5
star6.getValue()
star6.setValue("whatever") // 编译错误
star6.setValue("whatever")
一句编译器报错,这是因为:
如果在一个不变范型上使用星投影,那么它等于是+。
具体到 star6 上来说,相当于 和 ,自然是不能写到。
再来看一个例子:
val list:MutableList<*> = mutableListOf("1","2","3")
list[0] = "4" // 编译错误
道理是同样的。
@UnsafeVariance
正常情况下,以下代码中,setValue 方法是不能通过编译的:
class MyClass<out T>(private var t:T)
fun getValue(): T return this.t
fun setValue(t: T) this.t = T
因为 T 是 out, 不能用于方法参数,但通过 @UnsafeVariance 注解,可以突破编译器的这种限制:
fun setValue(t: T) this.t = T
这是因为范型擦除的缘故。
范型擦除
何谓范型擦除,看以下代码:
var myClass1: MyClass<Int> = MyClass(5)
var myClass2: MyClass<Any> = myClass1
println(myClass2.getValue())
myClass2.setValue("hello")
println(myClass2.getVlaue)
myClass1 的类型是MyClass,它只能存放 Int,但是当我们将它赋值给一个 MyClass 之后,就可以向其中放任何东西了。这是因为从字节码的层级,范型的真实类型其实被丢失了(范型擦除),不管什么类型对它来说统统都是 Object。
范型方法
不管类是不是范型的,它的函数都可以使用范型:
fun <T> getValue(item: T): T
return item
getValue 函数时一个顶层函数,调用时可以这样调用它:
val item = getValue<Int>(100) // 根据类型推断,可以简写成:getValue(100)
println(item)
范型约束
class MyClass<T: List<T>> // 范型约束:T 只能是 List 或 List 的子类
如果不指定范型约束,那么就是 Any?。
多个约束通过 where 关键字定义(同 swift):
class MyClass<T> where T:Comparable<T>, T: Any
以上是关于深入kotlin - 范型的主要内容,如果未能解决你的问题,请参考以下文章