Kotlin 类扩展实现原理

在 Kotlin 中当项目集成第三方 SDK 的时候,如果需要为其中某个类新增方法来可以通过 className.methodName(){}, 即 类名.方法名 的形式来扩展函数,那么同样和 Java 一样是 JVM 语言的 Kt 为什么就可以实现这种功能呢,以下为一个例子,借助它来详细探讨一下实现原理及细节。

open class Father {
    //定义成员函数
    open fun shout() = println("Father call shout()")
}
class Son : Father() {
    //子类重写父类成员函数
    override fun shout() {
        println("Son call shout()")
    }
}
// 定义子类和父类扩展函数
fun Father.eat() = println("Father call eat()")
fun Son.eat() = println("Son call eat()")

fun main() {
    val obj: Father = Son()
    obj.shout()
    obj.eat()
}

// 执行结果
Son call shout()
Father call eat()

在 IDEA 中,打开 Kt 字节码预览如下

public final class test/Son extends test/Father {
// 省略 Son 字节码细节
}

public class test/Father {
// 省略 Father 字节码细节
}

public final class test/Test16Kt {

  // Father 的类扩展实际实现
  public final static eat(Ltest/Father;)V
    // annotable parameter count: 1 (visible)
    // annotable parameter count: 1 (invisible)
    @Lorg/jetbrains/annotations/NotNull;() // invisible, parameter 0
   L0
    ALOAD 0
    LDC "$this$eat"
    INVOKESTATIC kotlin/jvm/internal/Intrinsics.checkParameterIsNotNull (Ljava/lang/Object;Ljava/lang/String;)V
   L1
    LINENUMBER 16 L1
    LDC "Father call eat()"
    ASTORE 1
   L2
    ICONST_0
    ISTORE 2
   L3
    GETSTATIC java/lang/System.out : Ljava/io/PrintStream;
    ALOAD 1
    INVOKEVIRTUAL java/io/PrintStream.println (Ljava/lang/Object;)V
   L4
   L5
    LINENUMBER 16 L5
    RETURN
   L6
    LOCALVARIABLE $this$eat Ltest/Father; L0 L6 0
    MAXSTACK = 2
    MAXLOCALS = 3

  // // Son 的类扩展实际实现
  public final static eat(Ltest/Son;)V
    // annotable parameter count: 1 (visible)
    // annotable parameter count: 1 (invisible)
    @Lorg/jetbrains/annotations/NotNull;() // invisible, parameter 0
   L0
    ALOAD 0
    LDC "$this$eat"
    INVOKESTATIC kotlin/jvm/internal/Intrinsics.checkParameterIsNotNull (Ljava/lang/Object;Ljava/lang/String;)V
   L1
    LINENUMBER 18 L1
    LDC "Son call eat()"
    ASTORE 1
   L2
    ICONST_0
    ISTORE 2
   L3
    GETSTATIC java/lang/System.out : Ljava/io/PrintStream;
    ALOAD 1
    INVOKEVIRTUAL java/io/PrintStream.println (Ljava/lang/Object;)V
   L4
   L5
    LINENUMBER 18 L5
    RETURN
   L6
    LOCALVARIABLE $this$eat Ltest/Son; L0 L6 0
    MAXSTACK = 2
    MAXLOCALS = 3

  // access flags 0x19
  public final static main()V
   L0
    LINENUMBER 21 L0
// 实例化 Son
    NEW test/Son
    DUP
// 调用 Son 类的 init 方法
    INVOKESPECIAL test/Son. ()V
// 检查转换为 Father 类型
    CHECKCAST test/Father
    ASTORE 0
   L1
    LINENUMBER 22 L1
    ALOAD 0
// 调用 Father.shot() ,但是因为实例为 Son ,所以执行的还是 Son 重写的 shot()
    INVOKEVIRTUAL test/Father.shout ()V
   L2
    LINENUMBER 23 L2
    ALOAD 0
<-- 问题 1 -->
    INVOKESTATIC test/Test16Kt.eat (Ltest/Father;)V
   L3
    LINENUMBER 24 L3
    RETURN
   L4
    LOCALVARIABLE obj Ltest/Father; L1 L4 0
    MAXSTACK = 2
    MAXLOCALS = 1

  // access flags 0x1009
  public static synthetic main([Ljava/lang/String;)V
    INVOKESTATIC test/Test16Kt.main ()V
    RETURN
    // 省略部分无关的实现
  // compiled from: test16.kt
}
����
�test�Test16Kt"*

上述代码示例的 kt 文件名为 Test16,在问题 1 ,我们类中的代码 obj.eat() 在字节码中实际上是调用了 Test16Kt.eat(Ltest/Father;)V ,那么根据这个规律可以得知,类扩展实际上生成了一个当前文件名+Kt 的 class,然后把已扩展的实例作为参数传递进去,具体我们可以查看 Test16Kt 类中 public final static eat(Ltest/Son;)V 和 public final static eat(Ltest/Father;)V,那么最后一个疑问,为什么 obj 是 Son 的实例却调用了父类的扩展函数,子类调用父类扩展函数的原因,根据类扩展的字节码实现可以得知这不是因为继承,实际原因是在申明时把类型设置为 Father,如果将代码改为 val obj = Son(),那么字节码中就是调用 INVOKESTATIC test/Test16Kt.eat (Ltest/Son;)V

你可能感兴趣的:(Kotlin 类扩展实现原理)