在 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.<init> ()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
网友评论