Kotlin 中的类属性(Properties)是如何实现的
在 kotlin 中,我们可以定义类属性,那么类属性在 class 文件中是如何实现的呢?
结论
val 的情形
class文件中,是否 生成getter?- 如果是
privateval,且用户没有自定义getter,则 不生成getter - 否则,会 生成
getter,有如下两种情况- 如果用户自己定义了
getter,则 生成 对应的getter - 如果用户 没有 自定义
getter,则 生成 默认的getter
- 如果用户自己定义了
- 如果是
class文件中,是否 生成backing field?- 如果有
property initializer(例如val name = "Someone"),则 生成backing field - 否则,如果用户没有定义
getter,则 生成backing field - 否则(也就是用户定义了
getter的情况),如果用户自定义的getter中使用了field这个标识符,则 生成backing field - 如果以上3种情况 都不成立 ,则 不生成
backing field
- 如果有
var 的情形
class文件中,是否 生成getter?- 如果是
privatevar,且用户没有自定义getter,则 不生成getter - 否则,会 生成
getter,有如下两种情况- 如果用户自己定义了
getter,则 生成 对应的getter - 如果用户 没有 自定义
getter,则 生成 默认的getter
- 如果用户自己定义了
- 如果是
class文件中,是否 生成setter?- 如果是
privatevar,且用户没有自定义setter,则 不生成setter - 否则,会 生成
setter,有如下两种情况- 如果用户自己定义了
setter,则 生成 对应的setter - 如果用户 没有 自定义
setter,则 生成 默认的setter
- 如果用户自己定义了
- 如果是
class文件中,是否 生成backing field?- 如果有
property initializer(例如var name = "Someone"),则 生成backing field - 否则,如果用户既没有定义
getter,也没有定义setter,则 生成backing field - 否则(也就是用户定义了
getter或者setter的情况),如果用户在自定义的getter或者自定义的setter中使用了field这个标识符,则 生成backing field - 如果以上3种情况 都不成立 ,则 不生成
backing field
- 如果有
代码
Properties 一文的 Getters and setters 小节 提到声明类属性的语法如下
1. 使用 property initializer 的情形
先看看使用 property initializer 的情形。
我们用如下代码来进行分析(请将代码保存为 A.kt)。
class Human {
val name = "张无忌";
var age = 18;
}
fun main(args: Array<String>) {
val human = Human()
human.age = 20
println("${human.name} 的年龄是 ${human.age}")
}
kotlinc A.kt 命令可以编译 A.kt。编译 A.kt 后,我们会看到如下的 class 文件
AKt.class
Human.class
查看 Human 类
我们可以用如下的命令查看 Human.class 的内容
javap -v -p Human
部分结果展示如下(常量池等部分已经略去)
{
private final java.lang.String name;
descriptor: Ljava/lang/String;
flags: (0x0012) ACC_PRIVATE, ACC_FINAL
RuntimeInvisibleAnnotations:
0: #23()
org.jetbrains.annotations.NotNull
private int age;
descriptor: I
flags: (0x0002) ACC_PRIVATE
public Human();
descriptor: ()V
flags: (0x0001) ACC_PUBLIC
Code:
stack=2, locals=1, args_size=1
0: aload_0
1: invokespecial #8 // Method java/lang/Object."<init>":()V
4: aload_0
5: ldc #10 // String 张无忌
7: putfield #14 // Field name:Ljava/lang/String;
10: aload_0
11: bipush 18
13: putfield #18 // Field age:I
16: return
LineNumberTable:
line 1: 0
line 2: 4
line 3: 10
line 1: 16
LocalVariableTable:
Start Length Slot Name Signature
0 17 0 this LHuman;
public final java.lang.String getName();
descriptor: ()Ljava/lang/String;
flags: (0x0011) ACC_PUBLIC, ACC_FINAL
Code:
stack=1, locals=1, args_size=1
0: aload_0
1: getfield #14 // Field name:Ljava/lang/String;
4: areturn
LineNumberTable:
line 2: 0
LocalVariableTable:
Start Length Slot Name Signature
0 5 0 this LHuman;
RuntimeInvisibleAnnotations:
0: #23()
org.jetbrains.annotations.NotNull
public final int getAge();
descriptor: ()I
flags: (0x0011) ACC_PUBLIC, ACC_FINAL
Code:
stack=1, locals=1, args_size=1
0: aload_0
1: getfield #18 // Field age:I
4: ireturn
LineNumberTable:
line 3: 0
LocalVariableTable:
Start Length Slot Name Signature
0 5 0 this LHuman;
public final void setAge(int);
descriptor: (I)V
flags: (0x0011) ACC_PUBLIC, ACC_FINAL
Code:
stack=2, locals=2, args_size=2
0: aload_0
1: iload_1
2: putfield #18 // Field age:I
5: return
LineNumberTable:
line 3: 0
LocalVariableTable:
Start Length Slot Name Signature
0 6 0 this LHuman;
0 6 1 <set-?> I
}
基于 javap 命令给出的结果,我们可以反推出对应的 java 代码是这样的 ⬇️
// 以下代码是我手动转化的,不保证绝对准确,仅供参考
// 类上有注解,这里略,有兴趣的读者朋友可以自行查看它的具体内容
public final class Human {
@org.jetbrains.annotations.NotNull
private final java.lang.String name;
private int age;
public Human() {
super();
this.name = "张无忌";
this.age = 18;
}
@org.jetbrains.annotations.NotNull
public final java.lang.String getName() {
return this.name;
}
public final int getAge() {
return this.age;
}
// 其实 “<set-?>” 这个变量名在 java 中并不合法。我是从 LocalVariableTable 属性看到这个变量名的,在这里我只是如实把它粘贴过来了
public final void setAge(int <set-?>) {
this.age = <set-?>;
}
}
在 kotlin 源码的 Human 类中,name 是 val,age 是 var。
在 Human.class 文件中,可以找到
- 和作为
val的name对应的field/getter - 和作为
var的age对应的field/getter/setter
查看 AKt 类
我们可以用如下的命令查看 AKt.class 的内容
javap -v -p AKt
部分结果展示如下(常量池等部分已经略去)
{
public static final void main(java.lang.String[]);
descriptor: ([Ljava/lang/String;)V
flags: (0x0019) ACC_PUBLIC, ACC_STATIC, ACC_FINAL
Code:
stack=2, locals=2, args_size=1
0: aload_0
1: ldc #9 // String args
3: invokestatic #15 // Method kotlin/jvm/internal/Intrinsics.checkNotNullParameter:(Ljava/lang/Object;Ljava/lang/String;)V
6: new #17 // class Human
9: dup
10: invokespecial #21 // Method Human."<init>":()V
13: astore_1
14: aload_1
15: bipush 20
17: invokevirtual #25 // Method Human.setAge:(I)V
20: new #27 // class java/lang/StringBuilder
23: dup
24: invokespecial #28 // Method java/lang/StringBuilder."<init>":()V
27: aload_1
28: invokevirtual #32 // Method Human.getName:()Ljava/lang/String;
31: invokevirtual #36 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
34: ldc #38 // String 的年龄是
36: invokevirtual #36 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
39: aload_1
40: invokevirtual #42 // Method Human.getAge:()I
43: invokevirtual #45 // Method java/lang/StringBuilder.append:(I)Ljava/lang/StringBuilder;
46: invokevirtual #48 // Method java/lang/StringBuilder.toString:()Ljava/lang/String;
49: getstatic #54 // Field java/lang/System.out:Ljava/io/PrintStream;
52: swap
53: invokevirtual #60 // Method java/io/PrintStream.println:(Ljava/lang/Object;)V
56: return
LineNumberTable:
line 7: 6
line 8: 14
line 9: 20
line 10: 56
LocalVariableTable:
Start Length Slot Name Signature
14 43 1 human LHuman;
0 57 0 args [Ljava/lang/String;
RuntimeInvisibleParameterAnnotations:
parameter 0:
0: #7()
org.jetbrains.annotations.NotNull
}
基于 javap 命令给出的结果,我们可以反推出对应的 java 代码是这样的 ⬇️
// 以下代码是我手动转化的,不保证绝对准确,仅供参考
// 类上有注解,这里略,有兴趣的读者朋友可以自行查看它的具体内容
public final class AKt {
public static final void main(@org.jetbrains.annotations.NotNull String[] args) {
kotlin.jvm.internal.Intrinsics.checkNotNullParameter(args, "args");
Human human = new Human();
human.setAge(20);
System.out.println(new StringBuilder().append(human.getName()).append(" 的年龄是 ").append(human.getAge()).toString());
}
}
从上面的 java 代码可以看到,main 方法中
- 通过
getter方法来读取age字段的值,通过setter方法来更新它(即age字段)的值 - 通过
getter方法类读取name字段的值
2. 自定义 getter/setter 的情形
kotlin 也支持用户自己定义的 getter/setter
我们在 A.kt 的基础上做一些调整,改动后的代码如下 ⬇️ (请将代码保存为 B.kt)
class Human {
val name = "张无忌"
get() = "明教教主" + field
var age = 18
get() = 18
set(value) {
// 没有什么特别的含义,只是想看看 class 文件中会变成什么样子
field = value / 2
}
}
fun main(args: Array<String>) {
val human = Human()
human.age = 20
println("${human.name} 的年龄是 ${human.age}")
}
kotlinc B.kt 命令可以编译 B.kt。编译后,会生成如下两个 class 文件。
BKt.class
Human.class
由于 main 方法里的内容并没有变化,我们只看 Human 类的内容。
如下的命令可以查看 Human.class 的内容。
javap -v -p Human
结果中与字段,方法相关的部分如下(常量池等部分已略去)。
{
private final java.lang.String name;
descriptor: Ljava/lang/String;
flags: (0x0012) ACC_PRIVATE, ACC_FINAL
RuntimeInvisibleAnnotations:
0: #23()
org.jetbrains.annotations.NotNull
private int age;
descriptor: I
flags: (0x0002) ACC_PRIVATE
public Human();
descriptor: ()V
flags: (0x0001) ACC_PUBLIC
Code:
stack=2, locals=1, args_size=1
0: aload_0
1: invokespecial #8 // Method java/lang/Object."<init>":()V
4: aload_0
5: ldc #10 // String 张无忌
7: putfield #14 // Field name:Ljava/lang/String;
10: aload_0
11: bipush 18
13: putfield #18 // Field age:I
16: return
LineNumberTable:
line 1: 0
line 2: 4
line 5: 10
line 1: 16
LocalVariableTable:
Start Length Slot Name Signature
0 17 0 this LHuman;
public final java.lang.String getName();
descriptor: ()Ljava/lang/String;
flags: (0x0011) ACC_PUBLIC, ACC_FINAL
Code:
stack=2, locals=1, args_size=1
0: new #25 // class java/lang/StringBuilder
3: dup
4: invokespecial #26 // Method java/lang/StringBuilder."<init>":()V
7: ldc #28 // String 明教教主
9: invokevirtual #32 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
12: aload_0
13: getfield #14 // Field name:Ljava/lang/String;
16: invokevirtual #32 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
19: invokevirtual #35 // Method java/lang/StringBuilder.toString:()Ljava/lang/String;
22: areturn
LineNumberTable:
line 3: 0
LocalVariableTable:
Start Length Slot Name Signature
0 23 0 this LHuman;
RuntimeInvisibleAnnotations:
0: #23()
org.jetbrains.annotations.NotNull
public final int getAge();
descriptor: ()I
flags: (0x0011) ACC_PUBLIC, ACC_FINAL
Code:
stack=1, locals=1, args_size=1
0: bipush 18
2: ireturn
LineNumberTable:
line 6: 0
LocalVariableTable:
Start Length Slot Name Signature
0 3 0 this LHuman;
public final void setAge(int);
descriptor: (I)V
flags: (0x0011) ACC_PUBLIC, ACC_FINAL
Code:
stack=3, locals=2, args_size=2
0: aload_0
1: iload_1
2: iconst_2
3: idiv
4: putfield #18 // Field age:I
7: return
LineNumberTable:
line 9: 0
line 10: 7
LocalVariableTable:
Start Length Slot Name Signature
0 8 0 this LHuman;
0 8 1 value I
}
基于 javap 命令给出的结果,我们可以反推出对应的 java 代码是这样的 ⬇️
// 以下代码是我手动转化的,不保证绝对准确,仅供参考
// 类上有注解,这里略,有兴趣的读者朋友可以自行查看它的具体内容
public final class Human {
@org.jetbrains.annotations.NotNull
private final java.lang.String name;
private int age;
public Human() {
super();
this.name = "张无忌";
this.age = 18;
}
@org.jetbrains.annotations.NotNull
public final java.lang.String getName() {
return new StringBuilder().append("明教教主").append(this.name).toString();
}
public final int getAge() {
return 18;
}
public final void setAge(int value) {
this.age = value / 2;
}
}
从反推出的 java 代码中可以看到,getName()/getAge()/setAge(int) 这些方法确实有对应的变化。所以用户可以自定义 getter/setter。
3. 不生成支持字段(backing fields)的情形
是否可以不生成与类属性对应的支持字段(backing fields)呢?
Backing fields 中提到
由此可以推断出
- 对作为
val的类属性,如果同时满足以下两点,就不会生成对应的字段- 不使用
property initializer - 自定义
getter,且在自定义的getter中不引用field变量
- 不使用
- 对作为
var的类属性,如果同时满足以下两点,就不会生成对应的字段- 不使用
property initializer - 自定义
getter和setter,且在自定义的getter/setter中,都不引用field变量
- 不使用
我们可以用以下代码进行验证(请将代码保存为 C.kt)
class Human {
val name: String
get() = "明教教主"
var age: Int
get() = 18
set(value) {
// just do nothing
}
}
kotlinc C.kt 命令可以编译 C.kt。编译之后会看到 Human.class。
我们用 javap -v -p Human 命令来查看 Human.class 的内容。
与字段/方法有关的内容如下(常量池等部分已略去)
{
public Human();
descriptor: ()V
flags: (0x0001) ACC_PUBLIC
Code:
stack=1, locals=1, args_size=1
0: aload_0
1: invokespecial #8 // Method java/lang/Object."<init>":()V
4: return
LineNumberTable:
line 1: 0
LocalVariableTable:
Start Length Slot Name Signature
0 5 0 this LHuman;
public final java.lang.String getName();
descriptor: ()Ljava/lang/String;
flags: (0x0011) ACC_PUBLIC, ACC_FINAL
Code:
stack=1, locals=1, args_size=1
0: ldc #15 // String 明教教主
2: areturn
LineNumberTable:
line 3: 0
LocalVariableTable:
Start Length Slot Name Signature
0 3 0 this LHuman;
RuntimeInvisibleAnnotations:
0: #13()
org.jetbrains.annotations.NotNull
public final int getAge();
descriptor: ()I
flags: (0x0011) ACC_PUBLIC, ACC_FINAL
Code:
stack=1, locals=1, args_size=1
0: bipush 18
2: ireturn
LineNumberTable:
line 6: 0
LocalVariableTable:
Start Length Slot Name Signature
0 3 0 this LHuman;
public final void setAge(int);
descriptor: (I)V
flags: (0x0011) ACC_PUBLIC, ACC_FINAL
Code:
stack=0, locals=2, args_size=2
0: return
LineNumberTable:
line 9: 0
LocalVariableTable:
Start Length Slot Name Signature
0 1 0 this LHuman;
0 1 1 value I
}
基于 javap 命令给出的结果,我们可以反推出对应的 java 代码是这样的 ⬇️
// 以下代码是我手动转化的,不保证绝对准确,仅供参考
// 类上有注解,这里略,有兴趣的读者朋友可以自行查看它的具体内容
public final class Human {
public Human() {
super();
}
@org.jetbrains.annotations.NotNull
public final java.lang.String getName() {
return "明教教主";
}
public final int getAge() {
return 18;
}
public final void setAge(int value) {
}
}
可见确实没有生成对应的字段。
4. private val/private var 的情形
Properties 中的 Backing properties 小节 里提到
On the JVM: Access to private properties with default getters and setters is optimized to avoid function call overhead.
由此可以推断,当使用 private val/private var 时,如果用户只用默认的 getter/setter,那么 class 文件中 不会 生成对应的 getter/setter
我们把 C.kt 修改为这样 ⬇️
class Human {
private val name: String = "张无忌"
private var age: Int = 18
}
我们用 kotlinc C.kt 命令来编译 C.kt,
然后再用 javap -p Human 命令来查看 Human.class 的内容(这里不需要用 -v 选项)。
结果如下 ⬇️
Compiled from "C.kt"
public final class Human {
private final java.lang.String name;
private int age;
public Human();
}
的确没有生成 getter/setter,符合预期。