泛型的基本用法

在一般的编程模式下,我们需要给任何一个变量指定一个具体的类型,而泛型允许我们在不指定具体类型的情况下进行编程,这样写出来的代码将会拥有更好的扩展性。

举个例子,List 是一个用于存放数据的列表,但是 List 并没有限制我们只能存放整型还是字符串类型的数据,因为它没有指定具体的类型,而是使用泛型来实现的。也正是因为如此,我们才能使用 List、List 之类的语法来构建具体类型的列表。

泛型主要有两种定义方式:一种是定义泛型类,另一种是定义泛型方法,使用的语法都是 <T> 。T 是一种约定俗成的写法(因为类型是 Type),其实可以替换成任意字母或单词。

定义泛型类:

  1. class MyClass<T> {
  2. fun method(param: T): T {
  3. return param
  4. }
  5. }

我们在使用的时候,就可以将泛型指定成为具体的类型。使用的代码如下:

  1. val myClass = MyClass<Int>()
  2. val result = myClass.method(123)

定义泛型方法:

  1. class MyClass {
  2. fun <T> method(param: T): T {
  3. return param
  4. }
  5. }

泛型方法的使用:

  1. val myClass = MyClass()
  2. val result = myClass.method<Int>(123)
  3. // 因为 Kotlin 的类型推导机制,所以可以从 123 推导出类型,不用写泛型 <Int>
  4. val result2 = myClass.method(123)

泛型上界

Kotlin 允许我们对泛型的类型进行限制,默认情况下开发者可以向泛型中传入任意的具体类型,但是有时我们需要限制开发者传入的具体类型必须是某个类型的子类,这就是泛型上界的作用。下面的例子中我们将泛型的上界设置为 Number 类型,代码如下:

  1. class MyClass {
  2. fun <T: Number> method(param: T): T {
  3. return param
  4. }
  5. }

此时我们就只能向泛型传入 Int、Double、Float等数字类型了,如果我们传入 String 类型的话,IDE 会报错提示我们: Type argument is not within bounds.

其实如果我们不指定上界,那么默认情况下,默认的泛型上界其实是 Any? 这个可空类型,如果我们需要要求传入的上界是不可空的,那我们需要手动将泛型上界设置为 Any 类型。

类委托和属性委托

委托是一种设计模式,它的基本理念是:操作对象不会自己去处理某段逻辑,而是会把工作委托给另一个辅助对象去处理。Kotlin 中将委托功能分成了类委托和属性委托。

类委托

类委托的核心思想是将一个类的具体实现委托给另一个类去完成。例如下面的例子:

  1. class MySet<T>(private val helperSet: HashSet<T>): Set<T> {
  2. override val size: Int
  3. get() = helperSet.size
  4. override fun contains(element: T) = helperSet.contains(element)
  5. override fun containsAll(elements: Collection<T>) = helperSet.containsAll(elements)
  6. override fun isEmpty() = helperSet.isEmpty()
  7. override fun iterator() = helperSet.iterator()
  8. }

上面的代码中我们定义了一个自己写的数据结构 MySet 类,它实现了 Set 接口,构造方法需要传入一个 HashSet 类型的对象。 MySet 所有需要实现的属性和方法都直接调用了 helperSet 对象的对应方法。这其实就是一种委托模式。

但是上面的例子中我们所有的属性和方法都使用了辅助对象的属性和方法,为什么不直接使用辅助对象呢?这么说确实没错,但是如果我们只是让大部分的方法采用辅助对象的实现,少部分的方法实现由自己来重写,甚至加入一些自己独有的方法,那么 MySet 就会成为一个全新的数据类型。这也是 组合代替继承 思想的一种实现。

但是这种写法需要我们将需要实现的方法一一委托给辅助对象,需要花费很多时间在这种模板代码上,这些模板代码是没有任何意义的,真正有价值的是需要自己重写的属性或者方法。为了解决这个问题,Kotlin 使用了关键字 by 。我们只需要在接口的声明后面加上 by 关键字,后面跟上委托的辅助对象,就可以免去之前所写的一系列模板代码了。所以下列代码和上面的示例代码表达了相同的意思:

  1. class MySet<T>(private val helperSet: HashSet<T>): Set<T> by helperSet {}

此时如果我们需要重写某个方法或者添加新的方法或属性,可以在花括号中直接编写或者重写。例如下面的代码,我们重写了 isEmpty() 方法,新建了一个 helloWorld() 方法。

  1. class MySet<T>(private val helperSet: HashSet<T>): Set<T> by helperSet {
  2. override fun isEmpty() = false
  3. fun helloWorld() {
  4. println("hello world")
  5. }
  6. }

属性委托

类委托的思想是将一个类的实现交给另一个类去完成,而属性委托的核心思想是将一个属性的具体实现委托给另一个类去完成。例如下面的例子:

  1. class MyClass {
  2. var p by Delegate()
  3. }

通过 by 关键字,我们可以将变量 p 的具体实现委托给 Delegate 类去完成。当我们调用 p 的时候,就会调用 Delegate 类的 getValue() 方法;当我们对 p 进行赋值的时候,就会调用 Delegate 类的 setValue() 方法。

因此,我们需要对 Delegate 的具体实现提出要求,具体的代码如下:

  1. class Delegate {
  2. var propValue: Any? = null
  3. operator fun getValue(myClass: MyClass, prop: KProperty<*>): Any? {
  4. return propValue
  5. }
  6. operator fun setValue(myClass: MyClass, prop: KProperty<*>, value: Any?) {
  7. propValue = value
  8. }
  9. }

这是一种标准的代码实现模板,在Delegate类中我们必须实现 get/setValue 方法,并且都要用 operator 关键字进行声明。

getValue() 方法要接收两个参数,第一个参数用于声明该 Delegate 类的委托功能可以在什么类中进行使用,这里写成 MyClass 表示只能在 MyClass 类中使用;第二个参数 KProperty<*> 是Kotlin中的一个属性操作类,可用于获取属性相关的值,在当前场景下用不到,但是必须声明。另外, <*> 这种泛型的写法表示你不知道或不关心泛型的具体类型,只是为了通过语法编译而已,有点类似于Java中的 <?> 写法,至于返回值可以声明成任何类型,根据需要去写即可。

setValue() 方法也是类似的,只不过他要接收三个参数,前两个参数与 getValue() 方法是一致的,最后一个参数表示具体要赋值给委托属性的值,这个值必须与 getValue() 方法返回的类型保持一致。

如果 p 属性是被 val 关键字声明的,那我们可以在 Delegate 类中不声明 setValue() 方法,声明 getValue() 方法。

懒加载

在学习了属性委托之后,我们可以了解一下Kotlin的懒加载方法。下面的代码中我们将一个比较费时的运算放进了懒加载的 Lambda 表达式中,这样我们就可以在第一次访问这个属性的时候,才去执行这段费时的运算,达到懒加载的效果。

  1. class MyClass {
  2. val p by lazy {
  3. val result = expensive() // 费时运算
  4. result
  5. }
  6. val q = expensive() // 费时运算
  7. }
  8. fun main() {
  9. val myClass = MyClass() // 此时就执行了 q 的费时运算
  10. println(myClass.p) // 第一次访问 p 的时候才执行费时运算,进行懒加载
  11. }

懒加载的原理

lazy 只是一个高阶函数,它接受一个Lambda表达式,并返回一个 Delegate 对象。当我们访问 p 的时候会调用 Delegate 类中的 getValue() 方法,而在 getValue() 方法中会调用我们传入的Lambda表达式,达到懒加载的效果。