Kotlin 中的数据类(data class) 在 class 文件中是什么样子?

104 阅读3分钟

Kotlin 中的数据类(data class) 在 class 文件中是什么样子?

kotlinlang.org 网站上有 一篇文章 介绍了数据类(data class)。 其中提到 compiler 会自动为数据类生成一些方法。

image.png

我们来验证一下,另外也看看 class 文件中的数据类是什么样子。

结论

class 文件中,数据类会有这些组成部分 ⬇️

  • copy() 方法和 copy$default(...) 方法,它们用于支持对象的 copy 操作
  • componentN() 方法,这个(这些)方法用于支持 destructure 操作
  • 其他
    • 字段
    • 构造函数
    • getter/settersetter 不一定出现)
    • toString()/hashCode()/equals(java.lang.Object) 方法

代码

这篇文章 中有如下代码

data class User(val name: String, val age: Int)

在此基础上,我加了一些代码,修改后内容如下 ⬇️ (请将其保存为 User.kt

data class User(val name: String, val age: Int)

fun main(args: Array<String>) {
  val user = User("John", 42)
  val (name, age) = user 
  println("name is: ${name}")
  println("age is: ${age}")
  val newUser = user.copy(age = 3)
}

kotlinc User.kt 命令编译 User.kt 后,会得到 User.class 文件和 UserKt.class 文件。 在 Intellij IDEA 中,使用 Show Kotlin Bytecode 功能可以对 User.class 文件执行 decompile 操作。得到的结果如下

import kotlin.Metadata;
import kotlin.jvm.internal.Intrinsics;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;

@Metadata(
   mv = {2, 2, 0},
   k = 1,
   xi = 48,
   d1 = {"\u0000 \n\u0002\u0018\u0002\n\u0002\u0010\u0000\n\u0000\n\u0002\u0010\u000e\n\u0000\n\u0002\u0010\b\n\u0002\b\n\n\u0002\u0010\u000b\n\u0002\b\u0003\b\u0086\b\u0018\u00002\u00020\u0001B\u0017\u0012\u0006\u0010\u0002\u001a\u00020\u0003\u0012\u0006\u0010\u0004\u001a\u00020\u0005¢\u0006\u0004\b\u0006\u0010\u0007J\t\u0010\f\u001a\u00020\u0003HÆ\u0003J\t\u0010\r\u001a\u00020\u0005HÆ\u0003J\u001d\u0010\u000e\u001a\u00020\u00002\b\b\u0002\u0010\u0002\u001a\u00020\u00032\b\b\u0002\u0010\u0004\u001a\u00020\u0005HÆ\u0001J\u0013\u0010\u000f\u001a\u00020\u00102\b\u0010\u0011\u001a\u0004\u0018\u00010\u0001HÖ\u0003J\t\u0010\u0012\u001a\u00020\u0005HÖ\u0001J\t\u0010\u0013\u001a\u00020\u0003HÖ\u0001R\u0011\u0010\u0002\u001a\u00020\u0003¢\u0006\b\n\u0000\u001a\u0004\b\b\u0010\tR\u0011\u0010\u0004\u001a\u00020\u0005¢\u0006\b\n\u0000\u001a\u0004\b\n\u0010\u000b"},
   d2 = {"LUser;", "", "name", "", "age", "", "<init>", "(Ljava/lang/String;I)V", "getName", "()Ljava/lang/String;", "getAge", "()I", "component1", "component2", "copy", "equals", "", "other", "hashCode", "toString"}
)
public final class User {
   @NotNull
   private final String name;
   private final int age;

   public User(@NotNull String name, int age) {
      Intrinsics.checkNotNullParameter(name, "name");
      super();
      this.name = name;
      this.age = age;
   }

   @NotNull
   public final String getName() {
      return this.name;
   }

   public final int getAge() {
      return this.age;
   }

   @NotNull
   public final String component1() {
      return this.name;
   }

   public final int component2() {
      return this.age;
   }

   @NotNull
   public final User copy(@NotNull String name, int age) {
      Intrinsics.checkNotNullParameter(name, "name");
      return new User(name, age);
   }

   // $FF: synthetic method
   public static User copy$default(User var0, String var1, int var2, int var3, Object var4) {
      if ((var3 & 1) != 0) {
         var1 = var0.name;
      }

      if ((var3 & 2) != 0) {
         var2 = var0.age;
      }

      return var0.copy(var1, var2);
   }

   @NotNull
   public String toString() {
      return "User(name=" + this.name + ", age=" + this.age + ')';
   }

   public int hashCode() {
      int result = this.name.hashCode();
      result = result * 31 + Integer.hashCode(this.age);
      return result;
   }

   public boolean equals(@Nullable Object other) {
      if (this == other) {
         return true;
      } else if (!(other instanceof User)) {
         return false;
      } else {
         User var2 = (User)other;
         if (!Intrinsics.areEqual(this.name, var2.name)) {
            return false;
         } else {
            return this.age == var2.age;
         }
      }
   }
}

其中的内容有以下几部分

  • name 字段和 age 字段
  • User 类的构造函数
  • nameage 字段对应的 getter 方法
  • component1()component2() 方法,分别与 nameage 字段对应
  • copy(String, int) 方法
  • copy$default(User, String, int, int, Object) 方法(⬅️ 它是一个静态合成方法)
  • toString()/hashCode()/equals(Obejct) 方法

字段和构造函数就不解释了,我们来看看剩余的部分

getter 方法

因为 User.kt 里的 nameage 都是 val, 所以 compiler 只生成对应的 getter 方法,而不会生成对应的 setter 方法。

componentN() 方法

component1() 方法和 component2() 方法的作用是什么呢?

UserKt.class 进行 decompile 操作会得到如下的结果

import kotlin.Metadata;
import kotlin.jvm.internal.Intrinsics;
import org.jetbrains.annotations.NotNull;

@Metadata(
   mv = {2, 2, 0},
   k = 2,
   xi = 48,
   d1 = {"\u0000\u0012\n\u0000\n\u0002\u0010\u0002\n\u0000\n\u0002\u0010\u0011\n\u0002\u0010\u000e\n\u0000\u001a\u0019\u0010\u0000\u001a\u00020\u00012\f\u0010\u0002\u001a\b\u0012\u0004\u0012\u00020\u00040\u0003¢\u0006\u0002\u0010\u0005"},
   d2 = {"main", "", "args", "", "", "([Ljava/lang/String;)V"}
)
public final class UserKt {
   public static final void main(@NotNull String[] args) {
      Intrinsics.checkNotNullParameter(args, "args");
      User user = new User("John", 42);
      String name = user.component1();
      int age = user.component2();
      System.out.println("name is: " + name);
      System.out.println("age is: " + age);
      User newUser = User.copy$default(user, (String)null, 3, 1, (Object)null);
   }
}

对比 kotlin 代码和通过 decompile 而得到的 java 代码,会发现 kotlin 代码中的 destructure 操作在 class 文件里是通过调用对应的 componentN() 方法来实现的 ⬇️

image.png

componentN() 方法在 PairTriple 中的应用

其实 PairTriple 也是数据类,可以对它们进行 destructure 操作。 我们可以用下方的代码验证一下,请将代码保存为 A.kt

fun main(args: Array<String>) {
  val pair = "Benjamin" to "Franklin"
  val (firstName, lastName) = pair
  println("First name: ${firstName}")
  println("Last name: $lastName")

  val triple = Triple("欧阳", "修", "永叔")
  val (姓, 名, 字) = triple
  println("姓: ${姓}")
  println("名: ${名}")
  println("字: ${字}")
}

kotlinc A.kt 可以编译 A.kt,对生成的 AKt.class 文件进行 decompile 操作,可以生成对应的 java 代码 ⬇️

image.png

对比两者,会发现在 kotlin 代码中对 Pair/Triple 对象进行的 destructure 操作, 在 class 文件中会转化为对 componentN() 方法的调用。

componentN() 方法在 java 类中的应用

如果我们在 java 代码中定义 componentN() 方法,那么在 kotlin 代码中也可以对这样的类进行 destructure 操作。下面是一个这样的例子

请将如下代码保存为 Point.java

public class Point {
  private double x;
  private double y;

  public Point(double x, double y) {
    this.x = x;
    this.y = y;
  }
 
  public double component1() {
    return x;
  }

  public double component2() {
    return y;
  }
}

请将如下代码保存为 B.kt

fun main(args: Array<String>) {
  val (x, y) = Point(1.0, 0.0)
  println("x is: ${x}, y is: ${y}")
}

如下两个命令可以编译 Point.javaB.kt

javac Point.java
kotlinc -cp . B.kt

对比 B.ktdecompile 而得到的 java 代码,会发现对 Point 类的对象也可以进行 destructure 操作(虽然 Point 是通过 java 代码来定义的)⬇️

image.png

copy(...) 方法和 copy$default(...) 方法

这两个方法和对象的复制有关。 在 Data classes 一文中的 Copying 小节 有如下内容 ⬇️

image.png

kotlin 代码中的 copy(name: String, age: Int) 方法可以对 User 对象进行 copy 操作。 在调用 copy(name: String, age: Int) 方法时,name 参数和 age 参数都可以不提供(如果不提供的话,对应的默认值会是当前 User 对象的那个字段)。

通过对比 User.ktdecompile 而得到的 java 代码, 我们会发现 User.kt 中的 user.copy(age = 3) 会转化为 java 代码里的 User.copy$default(user, (String)null, 3, 1, (Object)null) ⬇️

image.png

User.kt 中的 user.copy(age = 3)class 文件中是这样处理的 ⬇️

  • 调用 User 类中的 copy$default(...) 静态方法
    • User 类中的 copy$default(...) 方法先将参数默认值填充好
    • User 类中的 copy$default(...) 再调用 copy(String, int) 方法

image.png

参数默认值是如何填充的呢? var3 充当 mask,通过它就可以知道哪些入参使用了默认值(如果 (var3 & 1) != 0 则说明 name 要用默认值,如果 (var3 & 2) != 0 则说明 age 要用默认值)。

  • var1name 对应,调用方未提供,所以使用默认值
  • var2age 对应,调用方显式指定了它的值为 3

image.png

这类处理方式在 Kotlin 中的默认参数在 class 文件中是如何实现的? 一文也介绍过。

其他

文中有一张图是借助 mermaid.live/ 生成的,用到的代码如下 ⬇️

---
config:
  layout: dagre
---
flowchart LR
 subgraph s1["In _kotlin_ source code"]
        A["_**main**_ function calls _**user.copy(age = 3)**_"]
  end
 subgraph s2["In _class_ file"]
        X["_**main**_ function (in _**UserKt class**_) calls _**User.copy$default(...)**_"]
        
    subgraph s3["In _**copy$default(...)**_"]
      Y["Step1: Populate each parameter that needs default value"]
      Z["Step2: Call _**copy(...)**_ with proper parameters"]
    end
  end
    X --> s3
    s1 -- 转化为 --> s2

参考资料

kotlinlang.org 网站上关于以下内容的介绍

  1. Data classes
  2. Destructuring declarations
  3. Pair
  4. Triple