前言
本来计划用一篇文章来将泛型中所有的知识都讲解完,写到中间发现还是分两篇来写比较好。在深入浅出泛型的文章中我们已经将泛型的基础知识详细的介绍完了,这篇文章就算是Kotin
泛型的进阶篇吧~
坚持写博客真的需要一定的耐心,既然决定要把Kotlin
的语法知识写成一个专栏,那我就要坚持把它写完,也算是巩固和学习了。这篇文章给大家介绍Kotlin
中泛型的协变、逆变、类型投影、星投影,这部分的知识内容可能不太好理解。因为在实际开发的过程中,我们很少会涉及到自己去写这部分代码。很多时候都是我们在源码里看到,但是又不太理解其中的含义。笔者尽可能用一些简而易懂的语言去讲解这篇文章,希望大家都能牢牢的掌握这部分的知识,下面我们开始本篇文章的讲解。
1.协变和逆变的约定
在开始学习协变和逆变之前,我们有必要先来了解一个约定。当我们定义一个泛型类或者是泛型接口时。如果将泛型定义为一个方法的参数时,我们称该泛型在in
位置。将泛型作为方法的返回值时,我们称该泛型在out
位置。如下代码示例: 类似同样的约束在Java中是这么描述的:我们称只能从中读取 的对象为生产者 ,并称那些你只能写入 的对象为消费者。有了这个约定我们就可以继续下面的学习。
2.泛型的协变
在上一篇文章中我们介绍了泛型的类型擦除机制。例如集合List<String>
和List<Any>
它们的类型参数只会在编译期间保留,在运行期间他们都会被识别成一个List
。我们知道String
是Any
的子类但是List<String>
并不是List<Any>
的子类。为了让List<Sring>
成为List<Any>
的子类,我们需要使用特殊的关键字out
来修饰泛型。例如我们所熟悉的List
接口:
csharp
public interface List<out E> : Collection<E> { }
正是因为泛型E
在接口声明时使用了out
关键字来修饰,我们才可以让下面这段示例代码顺利的通过编译器的验证:
kotlin
fun main() {
val listAny = mutableListOf<Any>()
val listString = mutableListOf<String>()
// List<String> 是 List<Any> 的子类
listAny.addAll(listString)
}
但同时我们也向编译器保证了,我们只会将泛型E
作为方法的返回值,该类型参数是只读的。如果我们强行将该泛型作为方法的参数,Kotlin
编译器则会提示语法错误。例如我们将上面的示例BaseClass
中的泛型T
加上out
关键字来修饰: 在一个泛型类或者接口中,如果
A
是B
的子类,同时Class<A>
又是Class<B>
的子类,我们就称类Class
在该泛型上是协变的。 那么现在问题来了。泛型为什么需要协变呢?泛型协变的好处是什么呢?其实最主要的目的还是为了防止类型转换的隐患,考虑我们有如下场景的代码:
kotlin
fun main() {
val baseClass = BaseClass<Student>()
val student = Student()
baseClass.setData(student)
// sync data
syncData(baseClass)
}
private fun syncData(baseClass: BaseClass<Person>) {
val teacher = Teacher()
baseClass.setData(teacher)
}
在上面这段代码中,syncData()
方法接收了一个baseClass
参数。假设泛型T
在类BassClass
上是协变的情况下,我们又允许该泛型出现在in
位置,这样我们将BaseClass<Student>
类型的参数,传递给syncData()
方法就是合法的。而在syncData()
方法内部我们将一个teacher
对象传递给了setData()
方法,Student
是Person
类型,Teacher
也是Person
类型,这也是合法的,但是这种写法很明显会造成类型转换的异常。因为可能在你没有察觉的地方,有人这么去写了。所以如果我们声明了泛型是协变的,该泛型就应该是只读的,只能放在out
位置。
但是在一些特殊的情况下,我们又可以将一个协变的类型参数放在in
位置。比如我们很确定的是,我们只会用这个参数进行一些类型上的判断或者是属性的读取,而不会涉及到类型的转换,我们就可以这么使用。例如List
接口中的contains()
方法:
kotlin
public interface List<out E> : Collection<E> {
override val size: Int
override fun isEmpty(): Boolean
override fun contains(element: @UnsafeVariance E): Boolean
override fun iterator(): Iterator<E>
override fun containsAll(elements: Collection<@UnsafeVariance E>): Boolean
}
强行将一个声明为out
的泛型放在in
位置,需要使用@UnsafeVariance
注解来标记。这代表你告诉编译器,你确定要这么使用,但是随着带来的类型转换的风险也需要你自己去承担。
3.泛型的逆变
泛型的逆变,如果单单是从定义上来看,感觉很难理解。那么什么是逆变呢?如果类A
是类B
的子类,而Class<B>
又是Class<A>
的子类,那么我们就称该类在该泛型上是逆变的。如下图所示:
要想声明某个类在某个泛型上是逆变的,我们使用关键字
in
。下面我们就来看一个具体的示例:
首先我们声明了一个泛型类型的接口
Transform<T>
,并在该泛型接口中声明了一个transform
方法。该方法接收泛型作为参数,并返回一个了Int
类型的值。我们知道Student
是Person
的子类,但是Trasform<Person>
并不是Transfom<Student>
的子类。所以在上面的示例中我们将Tranform<Person>
类型的参数传递给transformAction
方法的时候,编译器提示了语法错误。而关键字in
正是用来处理这种场景的。当我们给泛型接口Transfrom<T>
的泛型T
添加上关键字in
的时候,编译器不再报错了。
同时我们也向编译器保证了,我们只会将泛型
T
放在in
位置(方法的参数),而不会放在out
位置(方法的返回值)。还是一样的思考,此时我们如果将泛型T
放在out
位置,又会带来怎样的安全隐患呢? 我们将上面的泛型接口Transfrom
稍作更改,我们看一下如下场景的示例:
和协变一样,我们也可以使用注解将
in
关键字修饰的泛型放在out
位置。而随之而来的类型转换风险也需要我们自己去承担。如我们在上图中用箭头标记的1 处,我们在匿名类内部返回了一个Teacher
对象,这显然是合法的,因为Teacher
类是Person
类的子类。当我们使用注解躲过了编译器的报错提示后,我们在transfrom
方法中却犯下了错误,如图中标记的3 处。我们将一个student
对象传递给了transfromAction
方法,可是在声明时我们明确定义了transform
方法返回一个Teacher
类型的对象。显然运行这段代码,会报如下类型转换的异常:
arduino
Exception in thread "main" java.lang.ClassCastException: class com.study.myapplication.bean.Teacher cannot be cast to class com.study.myapplication.bean.Student (com.study.myapplication.bean.Teacher and com.study.myapplication.bean.Student are in unnamed module of loader 'app')
at com.study.myapplication.bean.PersonKt.transformAction(Person.kt:31)
at com.study.myapplication.bean.PersonKt.main(Person.kt:26)
at com.study.myapplication.bean.PersonKt.main(Person.kt)
Teacher cannot be cast to class Student
,我们无法将Teacher
转化成Student
。事实上这些风险在设计之初,设计这些语法规范的开发人员已经帮我们考虑了这种安全隐患,我们只需要严格遵守规则就好。in
位置只能作为方法的参数来使用,out
位置只能作为方法的返回值来使用。如果你强行使用注解来逃避编译器的检查,那随之而来的类型转换的风险就需要我们自己来承担。关于泛型的逆变,在Kotlin
内置的API
中就有一个很好的示例,如Comparable
接口:
kotlin
public interface Comparable<in T> {
public operator fun compareTo(other: T): Int
}
open class Person(private val age:Int) : Comparable<Person> {
override fun compareTo(other: Person): Int {
return if (this.age == other.age) 0
else if (this.age > other.age) 1
else -1
}
}
class Student(private val age:Int) : Person(age)
class Teacher(private val age:Int) : Person(age)
fun main() {
val student = Person(20)
compare(student)
}
fun compare(student: Comparable<Student>) {
val jack = Student(18)
val result = student.compareTo(jack)
println("result = $result")
}
// 输出
result = 1
如上示例代码:我们使用逆变让Comparable<Person>
变成了Comparable<Student>
的子类,在compare()
方法中我们成功的比较了两个学生在年龄上的大小。
4.类型投影 -> 使用处形变
有时我们在声明一个泛型类的时候,该泛型类在该泛型上既不是协变的也不是逆变的。例如:Kotlin
中的内置数组Array
:
kotlin
class Array<T>(val size: Int) {
fun get(index: Int): T { ...... }
fun set(index: Int, value: T) { ...... }
}
这造成了一些不灵活性。假设我们在实际开发中定义了一个copy()
函数,我们想实现将一个数组中的值copy
到另外一个数组中去:
这里我们遇到了和前面一样的问题,
Int
是Any
的子类,但是Array<Int>
并不是Array<Any>
的子类,编译器是不允许这么操作的。这里我们就可以引出类型投影 的概念了,也就是在使用处,我们可以使Array<Int>
成为Array<Any>
的子类型,对于这样的使用场景我们就称之为类型投影 。这样我们就可以顺利的通过编译器的检查,因为我们这么做是合法的。
kotlin
fun main() {
val arrayFrom = arrayOf(0, 1, 2)
val arrayTo = Array<Any>(3) { }
copy(arrayFrom, arrayTo)
}
fun copy(from: Array<out Any>, to: Array<Any>) {
assert(from.size == to.size)
for (i in from.indices) {
to[i] = from[i]
}
}
一旦我们使用out
来投影一个类型。那么该类在使用处只可以调用其get()
方法,而不能调用set()
方法。因为在copy()
函数中我们可以给from
数组存放一个String
,这随时可能造成类型转换异常。int
是Any
的子类,String
也是Any
的子类,我们在from: Array<Any>
数组中存放一个String
这显然也是合法的。但是我们在外部已经明确指明了arrayFrom
数组的类型参数是Int
。当我们使用out
来投影Array
,那么Array
的使用将是受限的,同时也帮我们规避了类型转换的风险。
同样的我们也可以使用in
来投影一个类型,假设我们有一个fill()
函数如下:
kotlin
fun main() {
val arrayFill = Array<Any>(3) { }
fill(arrayFill, "str")
}
fun fill(dest: Array<in String>, value: String) {
dest[0] = value
val str = dest[0]
println(str)
}
我们在使用处让Array<Any>
成了Any<String>
的子类,这样我们在main()
函数中才可以成功的将Array<Any>
传递给fill()
函数。在fill()
函数中,我们只能设置一个String
类型给到dest
数组,我们取出来的自然也是个String
,而这里的限制就是虽然我们将Array<Any>
传递给了fill()
数组,但是在使用处,我们只能存放String
类型的数据。
5.星投影 -> 子类型的投影
掌握了协变和逆变以及类型投影的知识,我们再来看星投影就比较好理解了。关于星投影我们也可以称之为子类型的投影。
kotlin
interface Factory<out T: Person> {
fun create() : T
}
在Factory
接口中,T
是一个拥有上界Person
的协变类型参数,这意味着当T
未知时,我们可以安全的从中读取Person
的值。
kotlin
interface Factory<in T: Person> {
fun setInfo(info: T)
}
同样的,在此种情形下,T
是一个拥有上届Person
的逆变类型参数,这意味着当T
未知时,没有什么可以以安全的方式写入Factory<in T: Person>
。关于星投影这里笔者不打算扩展来讲解了,只要我们能熟练的掌握协变和逆变以及类型投影的知识,其在用法上的规则都是一致的。
总结
到这里总算是将泛型的知识完整的介绍完了。泛型所涉及的知识相对来说还是比较难理解的,但是再难的知识也是从简单开始的,只要我们熟练的掌握了泛型的基础知识,这些晦涩难懂的知识也会迎刃而解。好了,关于泛型的知识,我们就讲解到这里。下篇文章笔者打算介绍Kotlin
语法知识中的by
关键字,我们下期再见~