Kotlin 中的数据类(data class) 在 class 文件中是什么样子?
kotlinlang.org 网站上有
一篇文章 介绍了数据类(data class)。
其中提到 compiler 会自动为数据类生成一些方法。
我们来验证一下,另外也看看 class 文件中的数据类是什么样子。
结论
在 class 文件中,数据类会有这些组成部分 ⬇️
- copy() 方法和 copy$default(...) 方法,它们用于支持对象的
copy操作 - componentN() 方法,这个(这些)方法用于支持 destructure 操作
- 其他
- 字段
- 构造函数
getter/setter(setter不一定出现)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类的构造函数- 与
name及age字段对应的getter方法 component1()和component2()方法,分别与name及age字段对应copy(String, int)方法copy$default(User, String, int, int, Object)方法(⬅️ 它是一个静态合成方法)toString()/hashCode()/equals(Obejct)方法
字段和构造函数就不解释了,我们来看看剩余的部分
getter 方法
因为 User.kt 里的 name 和 age 都是 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() 方法来实现的 ⬇️
componentN() 方法在 Pair 和 Triple 中的应用
其实 Pair 和 Triple 也是数据类,可以对它们进行 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 代码 ⬇️
对比两者,会发现在 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.java 和 B.kt
javac Point.java
kotlinc -cp . B.kt
对比 B.kt 与 decompile 而得到的 java 代码,会发现对 Point 类的对象也可以进行 destructure 操作(虽然 Point 是通过 java 代码来定义的)⬇️
copy(...) 方法和 copy$default(...) 方法
这两个方法和对象的复制有关。 在 Data classes 一文中的 Copying 小节 有如下内容 ⬇️
kotlin 代码中的 copy(name: String, age: Int) 方法可以对 User 对象进行 copy 操作。
在调用 copy(name: String, age: Int) 方法时,name 参数和 age 参数都可以不提供(如果不提供的话,对应的默认值会是当前 User 对象的那个字段)。
通过对比 User.kt 和 decompile 而得到的 java 代码,
我们会发现 User.kt 中的 user.copy(age = 3) 会转化为 java 代码里的 User.copy$default(user, (String)null, 3, 1, (Object)null) ⬇️
User.kt 中的 user.copy(age = 3) 在 class 文件中是这样处理的 ⬇️
- 调用
User类中的copy$default(...)静态方法User类中的copy$default(...)方法先将参数默认值填充好User类中的copy$default(...)再调用copy(String, int)方法
参数默认值是如何填充的呢?
var3 充当 mask,通过它就可以知道哪些入参使用了默认值(如果 (var3 & 1) != 0 则说明 name 要用默认值,如果 (var3 & 2) != 0 则说明 age 要用默认值)。
var1和name对应,调用方未提供,所以使用默认值var2和age对应,调用方显式指定了它的值为3
这类处理方式在 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 网站上关于以下内容的介绍