接下来我将为你详细讲解 Kotlin 字节码层探究构造函数、成员变量和 init 代码块执行顺序的攻略。
背景
在 Kotlin 中,成员变量和 init 代码块是可以在类中定义的,而它们的执行顺序和构造函数有着密切的关系。在了解 Kotlin 字节码层探究构造函数、成员变量和 init 代码块执行顺序之前,我们先来回顾一下 Kotlin 中的构造函数。
Kotlin 中的构造函数
Kotlin 中的构造函数分为主构造函数和次构造函数。主构造函数是与类名一起声明的一部分,可以包含属性初始化代码和它们的默认值。次构造函数则必须委托给主构造函数或者其他次构造函数。
成员变量和 init 代码块
除构造函数之外,Kotlin 中的类还可以定义成员变量和 init 代码块。成员变量和 init 代码块的执行顺序与对象初始化有关,而对象初始化则与构造函数有直接的关系。因此,了解构造函数、成员变量和 init 代码块之间的执行顺序是非常重要的。
字节码层探究
在字节码层面上,Kotlin 中的对象初始化主要涉及到两个指令:invokespecial
和 invokevirtual
。其中,invokespecial
指令用于调用构造函数,而 invokevirtual
指令用于调用 init 代码块。
在下面的内容中,我们将通过如何使用 Javap 工具、反编译 Kotlin 代码以及分析字节码指令的方式来探究构造函数、成员变量和 init 代码块之间的执行顺序。
示例1
首先,我们来看下面这个 Kotlin 类的代码:
class MyClass(var i: Int = 0) {
val j: Int = i * 2
init {
print("init: $i")
}
}
fun main() {
val myClass = MyClass(3)
}
这个类包含一个主构造函数和一个 init 代码块。同时,类中还定义了一个成员变量 j,该成员变量的值是通过 i 计算得出的。
接下来,我们使用 Javap 工具查看字节码。在 Kotlin 中,使用 Javap 查看字节码需要使用针对 Kotlin 的 Javap 版本,例如 kotlinc -d . MyClass.kt
生成 MyClass.class 后,使用 javap -p -v MyClassKt
查看 MyClassKt 中的字节码。我们查看的是 init
方法,因为它和成员变量的赋值都是在构造函数执行之前的:
init<init>()V
L0
LINENUMBER 5 L0
ALOAD 0
INVOKESPECIAL kotlin/jvm/internal/DefaultConstructorMarker.<init> ()V
L1
ALOAD 0
ILOAD 1
PUTFIELD MyClass.i : I
L2
ALOAD 0
ALOAD 0
GETFIELD MyClass.i : I
ICONST_2
IMUL
PUTFIELD MyClass.j : I
L3
ALOAD 0
LDC "init: "
INVOKESTATIC kotlin/io/ConsoleKt.access$print(Ljava/lang/Object;Ljava/lang/Object;)V
L4
ALOAD 0
GETFIELD MyClass.i : I
INVOKESTATIC java/lang/Integer.valueOf(I)Ljava/lang/Integer;
LDC ""
INVOKESTATIC kotlin/text/StringsKt__StringBuilderKt.append(Ljava/lang/StringBuilder;Ljava/lang/Object;)Ljava/lang/StringBuilder;
LDC ""
INVOKESTATIC kotlin/text/StringsKt__StringBuilderKt.append(Ljava/lang/StringBuilder;Ljava/lang/String;)Ljava/lang/StringBuilder;
LDC "init: "
INVOKEVIRTUAL kotlin/text/StringBuilder.append(Ljava/lang/String;)Ljava/lang/StringBuilder;
L5
ALOAD 0
GETFIELD MyClass.i : I
INVOKESTATIC java/lang/Integer.valueOf(I)Ljava/lang/Integer;
LDC ""
INVOKESTATIC kotlin/text/StringsKt__StringBuilderKt.append(Ljava/lang/StringBuilder;Ljava/lang/Object;)Ljava/lang/StringBuilder;
L6
POP
L7
RETURN
L8
LOCALVARIABLE this LMyClassKt; L0 L8 0
MAXSTACK = 3
MAXLOCALS = 2
从上面的字节码中可以看到,invokespecial
指令调用了 kotlin/jvm/internal/DefaultConstructorMarker.<init>()V
方法,这是因为 Kotlin 中的类的构造函数会默认调用该方法。紧接着,ALOAD
和 PUTFIELD
指令分别将传入的参数 i 赋值到类的 i 属性上。然后,ALOAD
、GETFIELD
、ICONST_2
和 IMUL
指令分别计算并将结果赋值给类的 j 属性上。最后,在 init
代码块中,我们可以看到打印了 init:i,说明 init 代码块已经被执行。由于类中的成员变量 j 和 init 代码块的执行顺序 是和构造函数的调用顺序没有关系,所以 j 成员变量此时已被成功计算出来。
在 main 函数中,我们创建了一个 MyClass 的实例,并将参数传递给构造函数。由于我们没有定义次构造函数,所以实际上调用了 主构造函数。在构造实例时,首先会执行 构造函数,然后才会执行对象的 j 成员变量的赋值。最后,执行 init 代码块。因此,在 main 函数中,我们能够看到输出值为:init: 3
。
示例2
接下来,我们再看一个场景,该场景下我们定义两个 init 代码块:
class MyClass {
var i: Int = 0
init {
println("First init block i = $i")
}
constructor(i: Int) : this() {
this.i = i
}
init {
println("Second init block i = $i")
}
}
fun main() {
MyClass(42)
}
在这个类中,我们定义了两个 init 代码块。这两个代码块都有对 i 实例变量的引用,因此我们满足了条件:init 代码块为不同的变量赋值,而且它们的执行次序之间的关系应该是有一定的依赖关系。接下来,使用 Javap 查看字节码文件:
<init>()V
L0
LINENUMBER 5 L0
ALOAD 0
INVOKESPECIAL kotlin/jvm/internal/DefaultConstructorMarker.<init> ()V
L1
ALOAD 0
ICONST_0
PUTFIELD MyClass.i : I
L2
ALOAD 0
LDC "First init block i = "
INVOKEVIRTUAL java/io/PrintStream.println(Ljava/lang/String;)V
L3
ALOAD 0
GETFIELD MyClass.i : I
INVOKESTATIC java/lang/Integer.valueOf(I)Ljava/lang/Integer;
LDC ""
INVOKESTATIC kotlin/text/StringsKt__StringBuilderKt.append(Ljava/lang/StringBuilder;Ljava/lang/Object;)Ljava/lang/StringBuilder;
L4
ALOAD 0
GETFIELD MyClass.i : I
INVOKESTATIC java/lang/Integer.valueOf(I)Ljava/lang/Integer;
LDC ""
INVOKESTATIC kotlin/text/StringsKt__StringBuilderKt.append(Ljava/lang/StringBuilder;Ljava/lang/String;)Ljava/lang/StringBuilder;
L5
ALOAD 0
LDC "First init block i = "
INVOKEVIRTUAL kotlin/text/StringBuilder.append(Ljava/lang/String;)Ljava/lang/StringBuilder;
L6
ALOAD 0
GETFIELD MyClass.i : I
INVOKESTATIC java/lang/Integer.valueOf(I)Ljava/lang/Integer;
LDC ""
INVOKESTATIC kotlin/text/StringsKt__StringBuilderKt.append(Ljava/lang/StringBuilder;Ljava/lang/Object;)Ljava/lang/StringBuilder;
L7
INVOKEVIRTUAL kotlin/text/StringBuilder.toString ()Ljava/lang/String;
L8
INVOKESTATIC kotlin/io/ConsoleKt.println(Ljava/lang/String;)V
L9
RETURN
L10
LOCALVARIABLE this LMyClass; L0 L10 0
MAXSTACK = 5
MAXLOCALS = 1
<init>(I)V
L0
LINENUMBER 8 L0
ALOAD 0
INVOKESPECIAL MyClass.<init> ()V
L1
ALOAD 0
ILOAD 1
PUTFIELD MyClass.i : I
L2
ALOAD 0
LDC "Second init block i = "
INVOKEVIRTUAL java/io/PrintStream.println(Ljava/lang/String;)V
L3
ALOAD 0
GETFIELD MyClass.i : I
INVOKESTATIC java/lang/Integer.valueOf(I)Ljava/lang/Integer;
LDC ""
INVOKESTATIC kotlin/text/StringsKt__StringBuilderKt.append(Ljava/lang/StringBuilder;Ljava/lang/Object;)Ljava/lang/StringBuilder;
L4
ALOAD 0
GETFIELD MyClass.i : I
INVOKESTATIC java/lang/Integer.valueOf(I)Ljava/lang/Integer;
LDC ""
INVOKESTATIC kotlin/text/StringsKt__StringBuilderKt.append(Ljava/lang/StringBuilder;Ljava/lang/String;)Ljava/lang/StringBuilder;
L5
ALOAD 0
LDC "Second init block i = "
INVOKEVIRTUAL kotlin/text/StringBuilder.append(Ljava/lang/String;)Ljava/lang/StringBuilder;
L6
ALOAD 0
GETFIELD MyClass.i : I
INVOKESTATIC java/lang/Integer.valueOf(I)Ljava/lang/Integer;
LDC ""
INVOKESTATIC kotlin/text/StringsKt__StringBuilderKt.append(Ljava/lang/StringBuilder;Ljava/lang/Object;)Ljava/lang/StringBuilder;
L7
INVOKEVIRTUAL kotlin/text/StringBuilder.toString ()Ljava/lang/String;
L8
INVOKESTATIC kotlin/io/ConsoleKt.println(Ljava/lang/String;)V
L9
RETURN
L10
LOCALVARIABLE this LMyClass; L0 L10 0
LOCALVARIABLE i I L0 L10 1
MAXSTACK = 5
MAXLOCALS = 2
在上面的字节码中,我们可以看到,先是调用了 kotlin/jvm/internal/DefaultConstructorMarker.<init>()V
方法,然后初始化 i 的值,接着执行当中的第一个 init 代码块。最后,执行的是我们传入的 42 值赋值的构造方法中的第二个 init 代码块。所以,最后的输出结果为:
First init block i = 0
Second init block i = 42
总结
通过这两个示例,我们了解了 Kotlin 字节码层探究构造函数与成员变量和 init 代码块执行顺序。成员变量和 init 代码块的执行顺序与对象初始化有关,而对象初始化则与构造函数有直接的关系。在使用 Javap 工具查看字节码时,我们可以通过查看调用的指令来判断哪个代码块先执行,哪个代码块后执行。掌握了这些知识后,我们可以更好地理解 Kotlin 中的对象初始化过程。
本站文章如无特殊说明,均为本站原创,如若转载,请注明出处:Kotlin字节码层探究构造函数与成员变量和init代码块执行顺序 - Python技术站