kotlin入门潜修之类和对象篇—扩展及其原理

本文收录于 kotlin入门潜修专题系列,欢迎学习交流。

创作不易,如有转载,还请备注。

扩展

扩展是kotlin提供的有别于java的新功能。扩展能够在不继承类或实现接口的情况下,扩展该类的功能。kotlin中既支持方法扩展也支持属性扩展。

扩展方法

扩展方法的定义是基于类型定义的,它并不依赖于具体的对象,而是依附于具体的类型。如我们要为String类添加一个扩展方法lastChar,用于获取字符串的最后一个字符,可定义如下:

fun String.lastChar(): Char {//这里为String定义了一个扩展方法,该方法的功能就是获取字符串的最后一个字符
    return this[this.length - 1]//注意这里使用了this
}
//测试类,提供main方法
class Main {
    companion object {
        @JvmStatic fun main(args: Array) {
            println("hello".lastChar())//注意这里,打印'o'
        }
    }
}

从上面的代码可以看出,扩展方法的定义首先和普通的方法定义是一致的,只不过方法名不一样,扩展方法名要加上具体的类型,扩展方法属于什么类型就要加上什么类型,如String.lastChar(),就在lastChar()前面加上了String类型,表示lastChar方法是String的扩展方法,任何使用String的地方都可以使用该方法。

上面代码中,lastChar方法最后return的时候使用了this关键字,表示当前对象,那么这个当前对象是什么对象呢?实际上就是我们在调用lastChar方法的时候使用的对象,本例当中就是"hello"这个字符串对象。

kotlin中的扩展方法还可以泛型化(后面会有文章阐述泛型),比如我们要为MutaleList(kotlin中可变列表接口,类比于java中的list,本身被定义为了泛型)定义一个元素交换方法:

fun  MutableList.swap(index1: Int, index2: Int) {//这里为MutableList添加了泛型化的扩展方法,
    val tmp = this[index1]
    this[index1] = this[index2]
    this[index2] = tmp
}
//测试类,提供程序入口
class Main {
    companion object {
        @JvmStatic fun main(args: Array) {
            val list = mutableListOf(1, 2, 3)
            println(list)//打印[1, 2, 3]
            list.swap(1, 2)
            println(list)//打印[1, 3, 2]
        }
    }
}

扩展方法的派发机制

看到这个标题有点懵,什么是派发机制?如果懂java语言,可以结合java语言来看,如果不同也没有关系。这里大概阐述一下。

面向对象的语言一般会有两种派发机制,静态派发和动态派发。所谓静态派发可以想一下方法重载,即编译时就已经确定该调用哪个方法。而动态派发更多的体现在多态上,是指调用的方法只有在运行的时候才能确认。方法重载易于理解,参数不同,传入什么样的参数就调用对应参数个数的方法即可;可运行时确认调用方法是什么意思呢?

举个简单的例子,假如类A中有个方法叫m1,类B继承自A并复写了方法m1,那么假如我们实际对象是B,对象的类型是A,即我们通过父类型来调用方法m1的时候,该具体调用那个方法呢?如下所示:

open class A {//类A,有个m1方法
    open fun m1() {
        println("A.m1")
    }
}
class B : A() {//类B继承于A,并复写了m1方法
    override fun m1() {
        println("B.m1")
    }
}
//测试类,程序执行入口
class Main {
    companion object {
        @JvmStatic fun main(args: Array) {
            printM1(B())//注意这里,调用了printM1方法,实际传入的是B对象,而printM1接收的是类型为A(即B的父类)的参数,这么传是完全合法的,是常见的多态场景。
        }
        @JvmStatic fun printM1(a: A) {//printM1方法,接收的是父类型参数
            println(a.m1())//那么问题来了,这个到底是调用A的方法还是调用B的方法?
        }
    }
}

上面代码分析中抛出来了一个问题, println(a.m1())到底是调用了A中的方法还是B中的方法呢?通过打印结果(打印出了B.m1)我们发现,实际上调用的是B中的方法,为什么?

这是因为,在编译的时候编译器实际上是不知道printM1(a:A)方法中a的具体类型是什么,只有在真正运行的时候才知道:原来实际上传入的是B对象,所以会去调用B中的m1方法。这就是动态派发。

上面分析完成之后,我们来看下kotlin中扩展方法的派发机制,先看例子:

open class A {
}
class B : A() {
}
fun A.m1() {
    println("A.m1")
}
fun B.m1() {
    println("B.m1")
}
class Main {
    companion object {
        @JvmStatic fun main(args: Array) {
            printM1(B())
        }
        @JvmStatic fun printM1(a: A) {
            println(a.m1())//会打印'A.m1'
        }
    }
}

上面的代码执行完成后打印出了A.m1,完全没有收到实际类型的影响。因为我们实际传入的是B对象,但是却执行了A中的m1方法,这说明kotlin中的扩展方法是静态派发的。

如果kotlin中既存在扩展方法又存在类成员方法,且二者方法签名完全相同,那么kotlin会执行哪个方法呢?看下面这个例子:

class A {
    fun m1() {//定义了一个类成员方法m1
        println("A.m1: member")
    }
}
fun A.m1() {//这里又定义了一个扩展方法,和类成员方法签名一样
    println("A.m1: extension")
}
//测试类,程序执行入口
class Main {
    companion object {
        @JvmStatic fun main(args: Array) {
            print(A().m1())//打印A.m1: member
        }
    }
}

上面代码会打印A.m1: member,说明执行的是A类中的成员方法。其实这也是有道理的,如果优先执行扩展方法,那么用户可以随便写个同名方法就把标准库方法给覆盖掉了。

空类型接收器(Nullable Receiver)

看到这个标题也是一脸雾水,怎么突然冒出来一个空类型接收器?实际上我们在定义扩展方法的时候,都已经接触到receiver(接收器)了,所谓receiver(接收器)即是该扩展方法的归属,如定义扩展方法fun A.m1() {},则A就是receiver。

照例先看个例子:

fun Any?.toString(): String {//为Any?增加一个扩展方法toString,这意味着调用对象是可为null的
    if (this == null) return "test"
    return toString()
}
//测试类,程序执行入口
class Main {
    companion object {
        @JvmStatic fun main(args: Array) {
            println(null.toString())//注意,这里使用了null.toString
            println(100.toString())//这里,使用了非null对象
        }
    }
}

上面的代码看起来确实很神奇,竟然可以用null来调用toString方法!实际上上面两种调用方法是完全不一样的:null.toString()实际上调用的是Any?的扩展toString方法;而100.toString实际上调用的是Any中的toString方法,而不是扩展方法。

扩展属性

同扩展方法一样,kotlin也支持扩展属性,但是属性扩展只能通过复写get方法完成初始化,无法直接进行初始化,示例如下:

val Any.name: String//正确,为Any提供扩展属性,复写了get方法
    get() = "any"
val Any.name2: String = "any"//错误,编译器会提示name2属性没有后备字段(backing field)

上面代码中的注释已经大概说明了扩展属性无法直接初始化的原因,那为什么扩展属性没有后备字段呢?这是因为扩展属性并不是直接插入类中的,即不属于类,所以就没有办法获取到后备字段。只能通过getter和setter进行定义。

伴随对象扩展

伴随对象的扩展同普通扩展的定义一样,这里简单举一个为伴随对象扩展方法的例子,如下所示:

class Test {
    companion object {//为Test类下定义了一个伴随对象
    }
}
//为Test类下的伴随对象增加m1方法
fun Test.Companion.m1() {
    println("extension m1")
}
//测试类
class Main {
    companion object {
        @JvmStatic fun main(args: Array) {
            Test.m1()//打印extension m1
        }
    }
}

伴随对象扩展方法,唯一需要注意的是其接收器写法是外部类名+Companion(首字母大写)即可。

扩展的范围

通常情况下,我们都是作为top-level级的成员来定义扩展方法或属性的,然后在使用的地方import即可。那么如果扩展定义在类中,其使用范围是怎么样呢?

首先,在一个类中是可以为另一个类定义扩展的,对于这样的扩展,将会有多个隐式的接受器(receiver),这是因为对象的成员可以不通过限定符语法进行访问。这句话听起来很绕口,不理解没关系,先看个例子(请仔细看里面的注释):

class A {//class A,定义了一个成员方法m1
    fun m1() {
        println("A.m1")
    }
}
class B {//class B,其实例被称为调度接收器(dispatch receiver)
    fun m2() {//定义了一个成员方法m2
        println("B.m2")
    }
    fun A.m3() {//在类B中定义了一个类A的扩展方法m3,这个时候A的实例被称为扩展接收器( extension receiver)
        m1()//调用A.m1,这里没有通过限定符来访问m1方法
        m2()//调用B.m2
    }
    fun test(a: A) {
        a.m3()//调用A类型的扩展方法
    }
}
//测试入口
class Main {
    companion object {
        @JvmStatic fun main(args: Array) {
            val b = B()
            b.test(A())//diaoyongB类中的test方法
        }
    }
}

看完代码估计还是不太理解,确实,这块有点绕。首先解释下上面提到的几个名词:
1.调度接收器(dispatch receiver):在哪个类中定义的扩展,那个类的实例就叫做调度接收器。上面我们定义的B类型的实例就是调度接收器。
2.扩展接收器( extension receiver): 扩展方法所属的receiver的实例就是扩展接收器。上面我们定义的A类型的实例就是扩展接收器。
3.多个隐式的接收器是什么意思?上面代码中,因为扩展函数(m3)既有扩展接收器(A)的引用,又有调度接收器(B)的引用,所以既可以调用A中的成员也可以调用B中的成员。A和B都是接收器。

因为扩展方法既能访问扩展接收器成员又能访问调度接收器成员,那么如果二者存在一模一样的成员该怎么办?此时,我们可以通过限定符语法来访问。示例如下:

class A {
    fun m1() {
        println("A.m1")
    }
}
class B {
    fun m1() {
        println("B.m1")
    }
    fun A.m3() {
        m1()//默认调用A.m1
        [email protected]()//显示指定调用B.m1方法
    }
    fun test(a: A) {
        a.m3()
    }
}
//测试入口
class Main {
    companion object {
        @JvmStatic fun main(args: Array) {
            val b = B()
            b.test(A())
        }
    }
}

上述代码执行完成后,打印如下

A.m1
B.m1

这说明,如果存在冲突的时候,会优先调用扩展接收器的方法,可以通过显示指定调度接收器来调用调度接收器中的方法。

kotlin中扩展方法是可以被定义为open类型的,这就意味着子类可以复写扩展方法,那么如果遇到这种情况kotlin的调用机制又是怎样的呢?看个例子:

open class A {}
class SubA : A() {}//定义A类的子类SubA

open class B() {
    open fun A.m1() {//在B类中定义了A类的扩展
        println("A.m1 in B")
    }

    open fun SubA.m1() {//在B类中定义了SubA类的扩展
        println("SubA.m1 in B")
    }

    fun test(a: A) {//测试方法入口
        a.m1()
    }
}

class SubB : B() {//定义B类的子类SubB
    override fun A.m1() {//复写了父类B中的A类扩展方法m1
        println("A.m1 in SubB")
    }

    override fun SubA.m1() {复写了父类B中的SubA类扩展方法m1
        println("SubA in SubB")
    }
}
//测试入口
class Main {
    companion object {
        @JvmStatic fun main(args: Array) {
            B().test(A())//打印'A.m1 in B',很简单,调用B类中的test方法,test方法执行A类的扩展方法m1所以打印'A.m1 in B'
            B().test(SubA())//打印'A.m1 in B',因为扩展方法是静态派发的,所以test方法入参是什么类型就会调用该类型的方法,故打印结果同上面一样。
            SubB().test(A())//打印'A.m1 in SubB',SubB继承自B,故调用B类中的test方法,然后调用A的扩展方法,但是SubB此时复写了定义在B类中的A类扩展方法,所以在运行的时候会调用该SubB中的方法。
            SubB().test(SubA())//打印同上,分析也同上
        }
    }
}

代码中已经给到了详细分析,这里不再重复。只有一点,对于继承,可以认为扩展方法和普通方法一致。都会在运行时调用子类的实现(如果子类没有复写则调用父类实现)。

扩展可见性

对于扩展方法,kotlin采用和普通方法一样的可见性修饰规则。可以参见kotlin入门潜修之类和对象篇—权限修饰符

kotlin为什么要提供扩展

kotlin提供扩展的初衷就是为了解决java中一个常见的痛点。比如我们经常会写一大堆util类如StringUtil、FileUtil等等,包括java中的jdk都大量使用了这些util,如java中的Collections辅助类,先看段代码:

import java.util.Collections;
Collections.swap(list, Collections.binarySearch(list
                , Collections.max(anthoerList)), Collections.max(list));

上面代码的功能是先查找list是否包含有anthoerList中的最大值,然后和list中的最大值进行交互。

这里且先不谈代码的意义,从表面上看,这段代码是不是显着很啰嗦?Collections作为一个util类到处被引用。有朋友说可以直接导出静态类啊,这样就不用每次都带Collections了?

确实是这样,java本身支持直接导入static类,这样做以后代码的调用就可以简洁如下:

import static java.util.Collections.binarySearch;
import static java.util.Collections.max;
import static java.util.Collections.swap;
//真正调用代码
swap(list, binarySearch(list
                , max(athoerList)), max(list));

从上面代码可以看出swap方法的调用确实简洁不少,而import却急剧膨胀。而且语义已经模糊了,因为你不太容易知道swap到底是在哪儿定义的。这也是使用java不推荐static导入的原因。

那么理想的调用方式该是怎么样的呢?比如下面的调用方式:

//假如java代码这么来写
list.swap(list, binarySearch(list)
                , max(athoerList), max(list));

上面代码的写法就很简洁,语义也比较清楚:一看就知道,这是调用了list中的swap方法,目的是对list的某些元素进行swap等等。

该写法是达到了简洁代码的目的,但是我们又不想在list这个类中去额外增加诸如swap、binarySearch等方法,该怎么办?

kotlin正式基于上面缺陷和考虑,设计了扩展机制。

扩展的实现原理

本章节我们来看下kotlin中扩展的实现原理。

首先我们在Test.kt中定义一个ExtensionClass类,并为其添加一个getClassName的扩展方法,如下所示:

//注意,ExtensionClass位于Test.kt文件中
class ExtensionClass {
}
//这里为ExtensionClass定义了一个扩展方法getClassName
fun ExtensionClass.getClassName(){}

好了,代码已经写好了,那么从何处下手来了解扩展的实现原理呢?当然还是字节码文件。kotlin的代码最终都会编译成字节码文件,我们只需要看看getClassName的字节码实现即可明白,如下所示:

// ================com/juandou/mediapikcer/TestKt.class =================
// class version 50.0 (50)
// access flags 0x31
public final class com/juandou/mediapikcer/TestKt {//注意这里是TestKt类,并不是ExtensionClass类。
  // access flags 0x19
  public final static getClassName(Lcom/juandou/mediapikcer/ExtensionClass;)V//这里就是我们所定义的扩展方法!
    @Lorg/jetbrains/annotations/NotNull;() // invisible, parameter 0
   L0
    ALOAD 0
    LDC "receiver$0"
    INVOKESTATIC kotlin/jvm/internal/Intrinsics.checkParameterIsNotNull (Ljava/lang/Object;Ljava/lang/String;)V
   L1
    LINENUMBER 6 L1
    RETURN
   L2
    LOCALVARIABLE $receiver //这里定义了receiver
Lcom/juandou/mediapikcer/ExtensionClass; L0 L2 0
    MAXSTACK = 2
    MAXLOCALS = 1

  @Lkotlin/Metadata;(mv={1, 1, 13}, bv={1, 0, 3}, k=2, d1={"\u0000\u000c\n\u0000\n\u0002\u0010\u0002\n\u0002\u0018\u0002\n\u0000\u001a\n\u0010\u0000\u001a\u00020\u0001*\u00020\u0002\u00a8\u0006\u0003"}, d2={"getClassName", "", "Lcom/juandou/mediapikcer/ExtensionClass;", "app_debug"})
  // compiled from: Test.kt
}

从上面字节码可以得知如下结论:

  1. kotlin中的扩展方法,实际上会被编译为public final static的方法。
  2. 该public final static方法的位置并不是位于接收器类中(本例子接收器类即是ExtensionClass),而是位于其书写位置所对应的类中(本例子即是TestKt类,见字节码)。
  3. 为类添加扩展方法后,编译器会将该类指定为扩展方法的接收器。

那么有朋友可能会有疑问,既然编译成的方法位于书写位置对应的类中(即TestKt类),为什么我们还能通过接收器类(即ExtensionClass类)来调用呢?而不是通过书写位置对应的类调用呢?

比如我们调用上面的getClassName方法是通过如下方式:

   ExtensionClass().getClassName()

而不是:

TestKt().getClassName()

这个该如何解释呢?很简单,我们写一个调用扩展方法的示例,看下其对应的字节码就会明白了,示例如下:

class ExtensionClass {
}

fun ExtensionClass.getClassName(){}
//注意这里我们调用了ExtensionClass的扩展方法getClassName
fun test(){
    ExtensionClass().getClassName()
}

我们只需看getClassName调用处的字节码,如下所示:

// access flags 0x19
  public final static test()V
   L0
    LINENUMBER 9 L0
    NEW com/juandou/mediapikcer/ExtensionClass//new一个ExtensionClass对象
    DUP
    INVOKESPECIAL com/juandou/mediapikcer/ExtensionClass. ()V//调用ExtesionClass构造方法
    INVOKESTATIC com/juandou/mediapikcer/TestKt.getClassName (Lcom/juandou/mediapikcer/ExtensionClass;)V//!!!注意这里,是通过TestKt.getClass
Name类调用的!
   L1
    LINENUMBER 10 L1
    RETURN
   L2
    MAXSTACK = 2
    MAXLOCALS = 0

上面展示了test方法编译后的字节码,其中的两处注释就已经解开了前面我们的疑问:原来扩展方法的调用会被编译成其书写位置对应的类的静态调用(即TestKt.getClassName),kotlin只是为我们“包装”了一下而已,使得我们不必自己通过TestKt.getClassName来调用。

这么做显然是有道理的,因为我们可以在任何文件中为一个类添加扩展,显然很多时候我们并不一定能知道扩展的书写位置,也就无法通过书写扩展所在的类来调用扩展了。

至此,扩展的实现原理我们已经讲完。

你可能感兴趣的:(kotlin入门潜修之类和对象篇—扩展及其原理)