六、泛型和委托
1. 泛型的基本用法
泛型主要有两种定义方式:一种是定义泛型类,另一种是定义泛型方法,语法结构都是,T是一种约定俗成的泛型写法。
泛型类
class MyClass<T> {
fun method(param: T): T {
return param
}
}
1
2
3
4
5
MyClass就是一个泛型类,MyClass中的方法允许使用T类型的参数和返回值。
在调用MyClass类和method()方法时,就可以将泛型指定成具体的类型:
val myClass = MyClass<Int>()
val result = myClass.method(111)
1
2
泛型方法
class MyClass {
fun <T> method(param: T): T {
return param
}
}
1
2
3
4
5
调用
val myClass = MyClass()
val result = myClass.method<Int>(111)//由于Kotlin的类型推到机制,这里的Int可以省略
1
2
Kotlin还允许我们对泛型的类型进行限制。目前method()方法的泛型可以指定成任意类型,可以通过指定上界的方式来对泛型的类型进行约束,将method()方法的泛型上界设置为Number类型
class MyClass {
fun <T : Number> method(param: T): T {
return param
}
}
1
2
3
4
5
在默认情况下,所有的泛型都是可以指定成可空类型的,因为在不手动指定上界的时候,泛型的上界默认是Any?。如果想要让泛型的类型不可为空,只需将泛型的上界手动指定成Any。
下面对之前高阶函数手写的build函数改造一下,让它可以作用在所有类上。
fun StringBuilder.build(block: StringBuilder.() -> Unit) : StringBuilder {
block()
return this
}
//改成
fun <T> T.build(block: T.() -> Unit) : T {
block()
return this
}
1
2
3
4
5
6
7
8
9
10
2. 类委托和委托属性
委托是一种设计模式,他的基本理念是:操作对象自己不会去处理某段逻辑,而是会把工作委托给另外一个辅助对象去处理。Kotlin将委托功能分为两种:类委托和委托属性
类委托
核心思想是将一个类的具体实现委托给另一个类去完成。
Set是一个接口,使用它就要使用它具体的实现,比如HashSet。而借助于委托模式,我们可以轻松实现一个自己的实现类:
class MySet<T>(val helperSet: HashSet<T>) : Set<T> {
override val size: Int
get() = helperSet.size
override fun contains(element: T) = helperSet.contains(element)
override fun containsAll(elements: Collection<T>) = helperSet.containsAll(elements)
override fun isEmpty() = helperSet.isEmpty()
override fun iterator() = helperSet.iterator()
}
1
2
3
4
5
6
7
8
9
10
11
12
13
MySet构造函数接收一个HashSet参数,这就相当于一个辅助对象。如果我们只是让大部分方法实现调用辅助对象中的方法,少部分的方法实现由自己来重写,甚至加入一些自己独有的方法,那么MySet就会成为一个全新的数据结构类,这就是委托模式的意义所在。
如果接口中的待实现方法很多,这种写法就会很麻烦。在Kotlin中通过类委托的功能来解决。
Kotlin中委托使用的关键字是by,只需要在接口声明的后面使用by关键字,再接上受委托的辅助对象,就可以免去之前所写的一大堆代码,如下:
class MySet<T>(val helperSet: HashSet<T>) : Set<T> by helperSet {
}
1
2
这两段代码的效果是一模一样的,明显简化很多。如果我们要对某个方法重新实现,只需单独重写那一个方法,如下:
class MySet<T>(val helperSet: HashSet<T>) : Set<T> by helperSet {
fun helloWorld() = println("hello world")
override fun isEmpty() = false
}
1
2
3
4
这里我们新增了一个helloWorld()方法,并且重写了isEmpty()方法,其他Set接口中的功能,则和HashSet保持一致。这就是Kotlin的类委托所能实现的功能。
委托属性
核心思想是将一个属性的具体实现委托给另一个类去完成。
语法结构如下:
class MyClass {
var p by Delegate()
}
class Delegate {
var propValue: Any? = null
operator fun getValue(myClass: MyClass, prop: KProperty<*>): Any? {
return propValue
}
operator fun setValue(myClass: MyClass, prop: KProperty<*>, value: Any?) {
propValue = value
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
这种写法就代表着将p属性的具体实现委托给了Delegate类去完成。当调用p属性的时候会自动调用Delegate类的getValue()方法,当给p属性赋值的时候会自动调用Delegate类的setValue()方法。
Dlegate类的写法是一种标准的代码实现模板,在Delegate类中我们必须实现getValue()和setValue()方法,并且都要使用operator关键字声明。
getValue()方法要接收两个参数:第一个参数用于声明该Delegate类的委托功能可以在什么类中使用;第二个参数KProperty<*>是Kotlin中的一个属性操作类,可用于获取各种属性相关的值,在当前场景用不到,但是必须在方法参数上进行声明。另外< *>这种泛型的写法表示你不知道或者不关心泛型的具体类型,只为了通过语法编译,有点类似Java中<?>的写法。至于返回值可以声明成任何类型,根据具体逻辑编写。
setValue()方法有三个参数,前两个和getValue()方法相同,最后一个参数表示具体要赋值给委托属性的值,这个参数的类型必须和getValue()方法的返回值保持一致。
整个委托属性的工作流程就是这样实现的。
不过,存在一种情况可以不用在Delegate类中实现setValue()方法,那就是MyClass中的p属性是使用val关键字声明的。
3. 实现一个自己的lazy函数
懒加载技术,把想要延迟执行的代码放到by lazy代码块中,这样代码块中的代码在一开始的时候就不会执行,只有当懒加载对象首次被调用的时候,代码块中的代码才会执行。
val p by lazy { ... }
1
by是Kotlin的关键字,lazy是一个高阶函数。在lazy函数中会创建并返回一个Delegate对象,当我们调用p属性的时候,其实调用的是Delegate对象的getValue()方法,然后getValue()方法又会调用lazy函数传入的Lambda表达式,这样表达式的代码就会执行,并且调用p属性后得到的值就是Lambda表达式中最后一行代码的返回值。
根据懒加载原理,我们实现一个自己的lazy函数,新建一个Later.kt文件,并编写以下代码
class Later<T>(val block: () -> T) {
}
1
2
3
接下来在Later类中实现getValue()方法
class Later<T>(val block: () -> T) {
var value: Any? = null
operator fun getValue(any: Any?, prop: KProperty<*>): T {
if(value == null) {
value = block()
}
return value as T
}
}
1
2
3
4
5
6
7
8
9
getValue()方法的第一个参数指定成Any?类型,表示Later的委托功能在所有类中都可以使用。
由于懒加载技术是不会对属性进行赋值的,所以不用实现setValue()方法。
然后定义一个顶层函数
fun <T> later(block: () -> T) = Later(block)
1
later懒加载函数完成。
为了方便验证,写法如下:
val p by later {
Log.d("TAG", "run block")
"test later"
}
1
2
3
4
将这段代码放在一个Activity中,并在按钮的点击事件里调用属性p。
当Activity启动时,later函数中的日志不会打印,只有当首次点击按钮,日志才会打印。而再次点击按钮的时候,日志也不会再打印出来,因为代码块中的代码只会执行一次。
4. 泛型的高级特性
4.1 对泛型进行实化
JDK1.5中,Java才引入泛型功能,Java的泛型功能是通过类型擦除机制来实现的。就是说泛型对于类型的约束只在编译时期存在,JVM是识别不出来我们在代码中指定的泛型类型的。例如,我们创建一个List< String>集合,虽然在编译时期只能向集合中添加字符串类型的元素,但在运行期JVM只能识别出来它是个List。
所有基于JVM的语言,它们的泛型功能都是通过类型擦除机制来实现的,当然就包括了Kotlin,这种机制使得我们不可能使用a is T或者T::class.java这样的语法,因为T的实际类型在运行的时候已经被擦除了。
不同的是,Kotlin提供了内联函数的概念,内联函数中的代码会在编译的时候自动被替换到调用它的地方,这样就不存在泛型擦除的问题了,因为代码在编译后会直接使用实际的类型来替代内联函数中的泛型声明
fun foo() {
bar<String>() //m
}
inline fun <T> bar() {
//do something with T type //n
}
1
2
3
4
5
6
7
编译期n处代码会替换到m处,最终执行情况如下
fun foo() {
//do something with String type
}
1
2
3
这就意味着,Kotlin中是可以将内联函数中的泛型进行实例化的。
将泛型实化,首先该函数必须是内联函数,其次在声明泛型的地方必须加上reified关键字来表示该泛型要进行实化。
inline fun <reified T> getGenericType() {
}
1
2
上述函数中的泛型T就是一个被实化的泛型,具体实现什么效果呢,下面实现一个获取泛型实际类型的功能
inline fun <reified T> getGenericType() = T::class.java
1
下面进行测试
fun main() {
val result1 = getGenericType<String>()
val result2 = getGenericType<Int>()
println("result1 is $result1")
println("result2 is $result2")
}
1
2
3
4
5
6
以上代码打印结果会是:
result1 is class java.lang.String
result2 is class java.lang.Integer
1
2
4.2 泛型实化的应用
泛型实化功能允许我们在泛型函数中获得泛型的实际类型,这就意味着类似于a is T、T::class.java这样的语法是可行的。
启动一个Activity,一般会这么写
val intent = Intent(context, TestActivity::class.java)
context.startActivity(intent)
1
2
下面改写TestActivity::class.java的写法,新建一个reified.kt文件,编写如下代码
inline fun <reified T> startActivity(context: Context) {
val intent = Intent(context, T::class.java)
context.startActivity(intent)
}
1
2
3
4
Intent接收的第二个参数本来应该是一个具体的Activity的Class类型,但由于现在T已经是一个被实化的泛型了,可以直接使用T::class.java。
此时如果要启动TestActivity,可以这么写
startActivity<TestActivity>(context)
1
一般Intent是需要传参的,借助高阶函数就可实现。在reified.kt文件中重载startActivity()函数
inline fun <reified T> startActivity(context: Context, block: Intent.() -> Unit) {
val intent = Intent(context, T::class.java)
intent.block()
context.startActivity(intent)
}
1
2
3
4
5
这样在调用startActivity()函数的时候就可以在Lambda表达式中为Intent传参了
startActivity<TestActivity>(this) {
putExtra("param1", "data")
putExtra("param2", "111")
}
1
2
3
4
4.3 泛型的协变
先了解一个约定,一个泛型类或者泛型接口中的方法,它的参数列表是接收数据的地方,我们称它为in位置,而它的返回值是输出数据的地方,我们称它为out位置
interface MyClass<T> {
fun method(param: T): T//第一个T是in位置,第二个T是out位置
}
1
2
3
定义以下三个类:
open class Person(val name: String, val age: Int)
class Student(name: String, age: Int) : Person(name, age)
class Teacher(name: String, age: Int) : Person(name, age)
1
2
3
如果某个方法接收一个Person类型的参数,而我们传人一个Student 的实例,这是可行的。
如果某个方法接收一个List< Person>类型的参数,而我们传人一个 List< Student>的实例,在Java中是不允许这么做的,因为 List< Student>不能成为 List< Person>的子类,否则将可能存在类型转换的安全隐患,通过一个具体的例子进行说明。这里自定义个SimpleData 类,代码如下所示:
class SimpleData<T> {
private var data: T? = null
fun set(t: T) {
data = t
}
fun get(): T? = data
}
1
2
3
4
5
6
7
8
假设,如果编程语言允许向某个接收SimpleData< Person>参数的方法传入SimpleData< Student>的实例:
fun main() {
val student = Student("tom", 22)
val data = SimpleData<Student>()
data.set(student)
handleSimpleData(data)//实际上这行代码会报错
val studentData = data.get()
}
fun handleSimpleData(data: SimpleData<Person>) {
val teacher = Teacher("jack", 33)
data.set(teacher)
}
1
2
3
4
5
6
7
8
9
10
11
12
最后调用get()方法获取SimpleData< Student>获取它内部封装的Student数据,但是现在SimpleData< Student>中包含的却是一个Teacher实例,那么此时必然会产生类型转换异常。
为了杜绝这种安全隐患,Java是不允许使用这种方式传递参数的。即虽然Student是Person的子类,但是SimpleData< Student>并不是SimpleData< Person>的子类。
如果SimpleData在泛型T上是只读的,就不能设置Teacher实例,就不会存在类型转换的安全隐患。
协变的定义
假如定义了一个MyClass< T>的泛型类,其中A是B的子类,同时MyClass< A>又是MyClass< B>的子类,那么我们就可以称MyClass在T这个泛型上是协变的。
实现MyClass< A>又是MyClass< B>的子类,则需要让MyClass< T>类中的所有方法都不能接收T类型的参数,也就是让T只能出现在out位置上,而不能出现在in位置上。
下面改造一下SimpleData类
class SimpleData<out T>(val data: T?) {
fun get(): T? = data
}
1
2
3
这里在泛型T的声明前面加上out关键字,这就意味着T只能出现在out位置上,同时也意味着SimpleData在泛型T上是协变的。
由于这里我们使用的val关键字,所以构造函数中的泛型T仍然是只读的,另外,即使使用var,只要给它加上private修饰符,保证这个T对于外部是不可修改的,也是合法的写法。
修改测试代码
fun main() {
val student = Student("tom", 22)
val data = SimpleData<Student>(student)
handleSimpleData(data)//这里可以安全传递
val studentData = data.get()
}
fun handleSimpleData(data: SimpleData<Person>) {
val personData = data.get()
}
1
2
3
4
5
6
7
8
9
10
开头指出如果某个方法接收一个List< Person>类型的参数,而我们传人一个 List< Student>的实例,在Java中是不允许这么做的,但是在Kotlin中这么做是合法的,因为Kotlin已经默认给许多内置的API加上了协变声明,其中就包括各种集合的类和接口。
Kotlin中的List本身就是只读的,意味着它是可以协变的。如果想要给List添加数据,需要使用MutableList,下面看下List简化版源码
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>
public operator fun get(index: Int): E
}
1
2
3
4
5
6
7
List在泛型E的前面加上了out关键字,说明List在泛型E上是协变的。原则上声明了协变后,泛型E就只能出现在out位置上,可是在contains()方法中,泛型E仍然出现在了in位置上。
contains()方法目的非常明确,它只是为了判断当前集合中是否包含参数中传入的这个元素,并不会修改当前集合中的内容,因此这种操作实质上又是安全的。为了让编译器能够理解这种操作是安全的,这里在泛型E的前面又加上了一个@UnsafeVariance注解,这样编译器就会允许泛型E出现在in位置。
4.4 泛型的逆变
逆变的定义
假如定义了一个MyClass< T>的泛型类,其中A是B的子类型,同时MyClass< B>又是MyClass< A>的子类型,那么我们就可以称MyClass在T泛型上是逆变的。
举个例子,先定义一个Transformer接口,用于执行一些转换操作:
interface Transformer<T> {
fun transform(t: T) : String
}
1
2
3
参数T在经过transform()方法的转换后将会变成一个字符串。至于具体的转换逻辑,则由子类去实现,Transformer接口对此不关心。
对这个接口进行实现:
fun main() {
val trans = object : Transformer<Person> {
override fun transform(t: Person): String {
return "${t.name} ${t.age}"
}
}
handleTransformer(trans)//这里会提示语法错误
}
fun handleTransformer(trans: Transformer<Student>) {
val student = Student("Tom", 22)
val result = trans.transform(student)
}
1
2
3
4
5
6
7
8
9
10
11
12
13
这段代码从安全的角度来分析是没有问题的,因为Student是Person的子类,使用Transformer< Person>的匿名类实现将Student对象转换成一个字符串是安全的,但是在调用handleTransformer()方法的时候会提示语法错误,因为Transformer< Person>并不是Transformer< Student>的子类。
这时候就要用到逆变,修改Transformer接口中的代码:
interface Transformer<in T> {
fun transform(t: T) : String
}
1
2
3
这里在泛型T的声明前面加上了一个in关键字。意味着现在T只能出现再in位置上,同时也意味着Transformer再泛型T上是逆变的。
修改这个地方后,上面的代码就可以正常编译通过了,因为此时Transformer< Person>成为了Transformer< Student>的子类。
假设,逆变是允许泛型T出现在out位置上的,修改Transformer中的代码:
interface Transformer<in T> {
fun transform(name: String, age: Int) : @UnsafeVariance T
}
1
2
3
此时会产生什么样的安全隐患呢,如下代码:
fun main() {
val trans = object : Transformer<Person> {
override fun transform(name: String, age: Int): Person {
return Teacher(name, age)
}
}
handleTransformer(trans)
}
fun handleTransformer(trans: Transformer<Student>) {
val result = trans.transform("Tom", 22)
}
1
2
3
4
5
6
7
8
9
10
11
12
由于transform()方法的返回值要求的是一个Person对象,而Teacher是Person的子类,因此这种写法是合法的。但在handleTransformer()方法中,我们调用了Transformer< Student>的transform()方法,并传入name和age,期望得到一个Student对象的返回,然而实际上transform()方法返回的是一个Teacher对象,因此这里会造成类型转换异常。
逆变功能在Kotlin内置API中的应用
Comparable,是一个用于比较两个对象大小的接口,源码如下:
public interface Comparable<in T> {
public operator fun compareTo(other: T): Int
}
1
2
3
Comparab在T这个泛型上是逆变的。
————————————————
版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
原文链接:https://blog.csdn.net/bp1907/article/details/122333471