目录
一、泛型和委托
1.1、泛型的基本用法
两种定义方式:一种是定义泛型类,另一种是定义泛型方法,使用的语法结构都是<T>。 当然括号内的 T 并不是固定要求的,可以用任何英文字母或单词。 通常情况下,T 是一种约定俗成的泛型写法。
定义泛型类
//调用
val myClass = MyClass<Int>()
val result = myClass.method(123)
class MyClass<T> {
/**
* @param param T类型的参数
* @return T类型的返回值
*/
fun method(param: T): T {
return param
}
}
定义泛型方法
//调用
val myClass = MyClass()
//根据类型推导机制,还可省略<Int>:myClass.method(123)
val result = myClass.method<Int>(123)
class MyClass {
/**
* @param param T类型的参数
* @return T类型的返回值
*/
fun <T> method(param: T): T {
return param
}
}
泛型类型限制
class MyClass {
/**
* @param param T类型的参数
* @return T类型的返回值
*/
fun <T : Number> method(param: T): T {
return param
}
}
默认情况下,所有的泛型都是可以指定成可空类型的,泛型的上界默认是 Any?。 想要让泛型的类型不可为空,只需要将泛型的上界手动指定为 Any。
1.2、类委托和委托属性
委托基本理念:操作对象不会自己去处理某段逻辑,而是会把工作委托给另外一个辅助对象去处理。 Kotlin 中将委托功能分为了两种:类委托和委托属性。
类委托
举一个例子,Set 是一个接口,如果要使用的话,就需要使用其具体实现类,比如 HashSet。 这里我们首先定义一个 MySet 类并实现 Set 接口。 构造函数中接收了一个 HashSet 参数,这就相当于一个辅助对象。 然后在 Set 接口所有的方法实现中,都只是调用了辅助对象中相应的方法实现,这就是一种委托模式。 MySet 和 HashSet 都实现了 Set 接口,这里相当于 MySet 委托 HashSet 实现了自身的方法。
还可以只选择部分方法调用辅助对象中的方法,另一部分由自己来实现,甚至加入一些自己创建的方法。这时 MySet 就会成为一个全新的数据结构类,这就是委托模式的意义所在。
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()
}
对于方法数量较少时,上面的写法没有问题,但是一旦方法过多,就会显得很麻烦。这时在 Kotlin 中就可以通过类委托的功能来解决。
Kotlin 中委托使用的关键字是 by,我们只需要在接口声明的后面使用 by 关键字,再接上受委托的辅助对象,就可以免去手动重写这些方法了。使用了类委托的写法后,仍然可以对部分方法进行重写,或者添加自定义的方法。
class MySet<T>(val helperSet: HashSet<T>) : Set<T> by helperSet {
}
委托属性
类委托的核心思想是将一个类的具体实现委托给另一个类去完成,而委托属性的核心思想是将一个属性(字段)的具体实现委托给另一个类去完成。
这里使用 by 关键字连接了左边的 p 属性和右边的 Delegate 实例。这种写法就代表了将 p 属性的具体实现委托给了 Delegate 类去完成。当调用 p 属性的时候会自动调用 Delegate 类的 getValue 方法,当给 p 属性赋值的时候会自动调用 Delegate 类的 setValue 方法。
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
}
}
这是一种标准的代码实现模板,在 Delegate 类中我们必须实现 getValue 和 setValue 两个方法,并且都要用 operator 关键字声明。
getValue 方法接收两个参数:第一个参数声明该 Delegate 类的委托功能可以在什么类中使用,这里写成 MyClass 表示仅可在 MyClass 类中使用;第二个参数 KProperty<*>) 是 Kotlin 中的一个属性操作类,可用于获取各种属性相关的值,在当前场景下用不上,但是必须在方法参数上声明。至于返回值可以根据逻辑声明成任何类型。<*> 表示不关心泛型的具体类型。
setValue 方法接收三个参数:前两个参数跟 getValue 一样,最后一个参数表示具体要赋值给委托属性的值,这个参数的类型必须和 getValue 方法返回值保持一致。
最后还有一种情况,当 MyClass 中的 p 属性是使用 val 关键字声明时,就只需要实现 getValue 方法,不用实现 setValue 方法了。
1.3、关于 lazy 函数
by lazy 代码块是 Kotlin 提供的一种懒加载技术,代码块中的代码一开始并不会执行,只有当 names 变量首次被调用时才会执行,并且会将代码块中最后一行代码的返回值赋给 names。
private val names by lazy {
val list = ArrayList<String>()
list.add("张三")
list.add("李四")
list.add("王五")
list
}
结合 Kotlin 的委托功能,可以看出 by lazy 的基本语法结构。实际上 by lazy 并不是连在一起的关键字,只有 by 才是 Kotlin 中的关键字,lazy 在这里只是一个高阶函数。
val p by lazy { ... }
还可以使用委托功能实现一个自己的 lazy 函数。
二、使用 infix 函数构建更可读的语法
2.1、infix 是什么
在 String 类中有一个 startsWith 函数,用于判断一个字符串是否是以某个指定参数开头的。
if ("Hello Kotlin".startsWith("Hello")) {
}
infix 函数有两个比较严格的限制:首先,infix 函数不能定义成顶层函数,必须是某个类的成员函数,可以使用扩展函数的方式将它定义到某个类当中;其次,infix 函数必须接收且只能接收一个参数,至于参数类型是没有限制的。
这里自定义了一个 String 类的扩展函数 beginsWith,也是用来判断一个字符串是否是以某个指定参数开头的,其内部实现就是调用的 String 类的 startsWith 函数。在用 infix 关键字修饰后,不仅可以用传统的调用方式,还可以使用一种更接近英语的语法来调用,让代码更加具有可读性。
infix fun String.beginsWith(prefix: String) = startsWith(prefix)
if ("Hello Kotlin" beginsWith "Hello") {
}
上面的示例是比较简单的,复杂一些,比如这里有一个集合,如果想要判断集合中是否包括某个指定元素,代码如下。
val list = listOf("张三", "李四", "王五")
if (list.contains("李四")) {
}
借助 infix 函数让这段代码变得更有可读性,可以像这样写。在这里给 Collection 接口添加了一个扩展函数,这是因为 Collection 是 Java 以及 Kotlin 所有集合的总接口,因此给 Collection 添加一个 has() 函数,那么所有集合的子类就都可以使用这个函数了。
infix fun <T> Collection<T>.has(element: T) = contains(element)
val list = listOf("张三", "李四", "王五")
if (list has "李四") {
}
2.2、mapOf 函数中的 A to B
在 mapOf 函数中允许使用 A to B 这样的语法来构建键值对。查看源码,可以看到这里使用了定义泛型函数的方式将 to 函数定义到了 A 类型下,并且接收一个 B 类型的参数。因此 A 和 B 可以是两种不同类型的泛型,也就使得我们可以构造出 ”字符串 to 整型“ 这样的键值对。
public infix fun <A, B> A.to(that: B): Pair<A, B> = Pair(this, that)
to 函数的实现也很简单,就是创建并返回了一个 Pair 对象,也就是说,A to B 这样的语法结构实际上得到的是一个包含 A、B 数据的 Pair 对象。而 mapOf 函数实际上接收的也正是一个 Pair 类型的可变参数列表。
三、泛型的高级特性
3.1、对泛型进行实化
所有基于 JVM 的语言,它们的泛型功能都是通过类型擦除机制来实现的。类型擦除机制,是说泛型的约束只在编译时期存在,在运行时给泛型指定的实际类型是会被擦除的。由于这种机制,使得在 Kotlin 中的 a is T 或者 T::class.java 这样的语法本来是不能使用的,因为在运行时 T 并没有一个实际类型,能够使用是因为 Kotlin 又提供了一个内联函数的概念。
只有在内联函数中才能对泛型进行实化,因为内联函数中的代码在编译时会自动替换到调用它的地方,会直接使用实际的类型来替代内联函数中的泛型声明。
上面的代码会被替换成如下。
其次,要将泛型实化,除了是内联函数,还必须在声明泛型的地方加上 reified 关键字来表示该泛型要进行实化。比如这里定义了一个返回当前指定泛型的实际类型的函数 getGenericType。
inline fun <reified T> getGenericType() = T::class.java
val result1 = getGenericType<String>()
val result2 = getGenericType<Int>()
3.2、泛型的协变
一个泛型类或者泛型接口中的方法,它的参数列表是接收数据的地方,可以称之为 in 位置,返回值是输出数据的地方,可以称之为 out 位置。
首先定义三个类,一个父类 Person,两个子类 Student 和 Teacher。
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)
我们知道,如果某个方法接收一个 Person 类型的参数,那么传入一个 Student 对象是合法的,因为 Student 是 Person 的子类。如果某个方法接收一个 List<Person> 类型的参数,而传入一个 List<Student> 的对象,却是不合法的,因为这时存在类型转换的安全隐患。也就是说,即使 Student 是 Person 的子类,List<Student> 也不是 List<Person> 的子类。
在 Kotlin 中,假如定义了一个 MyClass<T> 的泛型类,其中 A 是 B 的子类,同时 MyClass<A> 又是 MyClass<B> 的子类,那么就可以称 MyClass 在 T 这个泛型上是协变的。要让 MyClass<A> 成为 MyClass<B> 的子类,,就需要让 MyClass<T> 类中的所有方法都不能接收 T 类型的参数。换句话说,T 只能出现在 out 位置,不能出现在 in 位置。实现的方法很简单,只需要在泛型的声明前加上 out 关键字,就意味着 MyClass 在泛型 T 上是协变的。
由于泛型 T 不能出现在 in 位置上,因此也就不能使用 set 方法为 data 的参数赋值了,这里改成使用构造函数的方式来赋值,然而在构造函数中的泛型 T 也是在 in 位置上,所以还应该使用 val 关键字来修饰,这样在构造函数中的泛型 T 仍然是只读的。即使是使用了 var 关键字,只要加上 private 关键字,保证这个泛型 T 对外部而言是不可修改的,也是合法的写法。
class SimpleData<out T>(val data: T?) {
fun get(): T? {
return data
}
}
在 Java 中,如果某个方法接收一个 List<Person> 类型的参数,而传入一个 List<Student> 类型的对象,这样做是不允许的。而在 Kotlin 中却是合法的,Kotlin 已经默认给许多内置的 API 加上了协变声明,其中就包含了各种集合的类和接口。
3.3、泛型的逆变
从定义上看,逆变和协变完全相反。假如定义了一个 MyClass<T> 的泛型类,其中 A 是 B 的子类,不同的是, MyClass<B> 又是 MyClass<A> 的子类那么就可以称 MyClass 在泛型 T 上是逆变的。详情待更。。。
interface Transformer<in T> {
fun transform(t: T): String
}