[Kotlin] 类属性(Properties)是如何实现的

124 阅读10分钟

Kotlin 中的类属性(Properties)是如何实现的

kotlin 中,我们可以定义类属性,那么类属性在 class 文件中是如何实现的呢?

结论

val 的情形

  • class 文件中,是否 生成 getter
    • 如果是 private val,且用户没有自定义 getter,则 不生成 getter
    • 否则,会 生成 getter,有如下两种情况
      1. 如果用户自己定义了 getter,则 生成 对应的 getter
      2. 如果用户 没有 自定义 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
    • 如果是 private var,且用户没有自定义 getter,则 不生成 getter
    • 否则,会 生成 getter,有如下两种情况
      1. 如果用户自己定义了 getter,则 生成 对应的 getter
      2. 如果用户 没有 自定义 getter,则 生成 默认的 getter
  • class 文件中,是否 生成 setter
    • 如果是 private var,且用户没有自定义 setter,则 不生成 setter
    • 否则,会 生成 setter,有如下两种情况
      1. 如果用户自己定义了 setter,则 生成 对应的 setter
      2. 如果用户 没有 自定义 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 小节 提到声明类属性的语法如下

image.png

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 类中,namevalagevar。 在 Human.class 文件中,可以找到

  1. 和作为 valname 对应的 field/getter
  2. 和作为 varage 对应的 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 中提到

image.png

由此可以推断出

  • 对作为 val 的类属性,如果同时满足以下两点,就不会生成对应的字段
    1. 不使用 property initializer
    2. 自定义 getter,且在自定义的 getter不引用 field 变量
  • 对作为 var 的类属性,如果同时满足以下两点,就不会生成对应的字段
    1. 不使用 property initializer
    2. 自定义 gettersetter,且在自定义的 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.

image.png

由此可以推断,当使用 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,符合预期。

参考资料