Kotlin
中的默认参数在 class
文件中是如何实现的?
kotlin
的方法中可以使用默认参数,那么这一特性在 class
文件中是如何做到的呢?本文对此进行介绍。
结论
- 会有一个静态合成方法负责将用户未提供的参数替换为对应的默认值
- 这个替换过程用到了位运算
代码
我们用以下的 kotlin
代码来进行探索(请将代码保存为 A.kt
)
kotlin
class A {
// 此方法有 5 个参数 ➡️ p1/p2/p3/p4/p5
// 只有 p2/p4 有默认值
fun aFunctionWithDefaultParameters(
p1: String,
p2: String = "p2's default value",
p3: String,
p4: String = "p4's default value",
p5: String,
) {
}
// 当 callerFunction() 调用 aFunctionWithDefaultParameters(...) 时,
// 会显式提供 p1/p2/p3/p5 参数,
// 而 p4 参数会使用默认值
fun callerFunction() {
aFunctionWithDefaultParameters(
p1 = "placeholder for p1",
p2 = "placeholder for p2",
p3 = "placeholder for p3",
p5 = "placeholder for p5"
)
}
}
kotlinc A.kt
命令可以编译 A.kt
。编译之后,我们会看到 A.class
文件。 javap -p A
命令可以查看 A.class
中的简要内容。 结果如下
text
Compiled from "A.kt"
public final class A {
public A();
public final void aFunctionWithDefaultParameters(java.lang.String, java.lang.String, java.lang.String, java.lang.String, java.lang.String);
public static void aFunctionWithDefaultParameters$default(A, java.lang.String, java.lang.String, java.lang.String, java.lang.String, java.lang.String, int, java.lang.Object);
public final void callerFunction();
}
在 class
文件里,A
类有 4
个方法
- 构造函数
aFunctionWithDefaultParameters(...)
方法aFunctionWithDefaultParameters$default(...)
⬅️ 是静态方法callerFunction()
方法
用 javap -v -p A
命令可以看到每个方法的 Code
属性(具体内容可以参考 本文的最后一小节)。 利用 javap -v -p A
命令给出的结果,我们可以手动转化出对应的 java
代码 ⬇️
java
// 以下 java 代码是我手动转化的,不保证绝对准确,仅供参考。
// 类上有注解,这里略去
public final class A {
public A() {
super();
}
public final void aFunctionWithDefaultParameters(
@org.jetbrains.annotations.NotNull String p1,
@org.jetbrains.annotations.NotNull String p2,
@org.jetbrains.annotations.NotNull String p3,
@org.jetbrains.annotations.NotNull String p4,
@org.jetbrains.annotations.NotNull String p5
) {
kotlin.jvm.internal.Intrinsics.checkNotNullParameter(p1, "p1");
kotlin.jvm.internal.Intrinsics.checkNotNullParameter(p2, "p2");
kotlin.jvm.internal.Intrinsics.checkNotNullParameter(p3, "p3");
kotlin.jvm.internal.Intrinsics.checkNotNullParameter(p4, "p4");
kotlin.jvm.internal.Intrinsics.checkNotNullParameter(p5, "p5");
}
// 下面这个方法是一个合成方法,照理说源码中不会出现,但是本文需要展示它的逻辑,所以我还是把它写出来了
public static void aFunctionWithDefaultParameters$default(A var0, String var1, String var2, String var3, String var4, String var5, int var6, java.lang.Object var7) {
if ((var6 & 2) != 0) {
var2 = "p2's default value";
}
if ((var6 & 8) != 0) {
var4 = "p4's default value";
}
var0.aFunctionWithDefaultParameters(var1, var2, var3, var4, var5);
}
// 注意:在 kotlin 的代码里,我们指定的参数是
// p1 = "String placeholder for p1"
// p2 = "String placeholder for p2"
// p3 = "String placeholder for p3"
// p5 = "String placeholder for p5"
// 与这里 ⬇️ 的参数颇有差异,下文会有说明
public final void callerFunction() {
aFunctionWithDefaultParameters$default(
this,
"placeholder for p1",
"placeholder for p2",
"placeholder for p3",
null,
"placeholder for p5",
8,
null
);
}
}
初步观察一下,逻辑应该是这样的 ⬇️

复杂的地方在 步骤一,我们看一下这一步的细节
"步骤一" 的分析
aFunctionWithDefaultParameters$default(...)
对应的 java
代码如下 ⬇️
java
public static void aFunctionWithDefaultParameters$default(A var0, String var1, String var2, String var3, String var4, String var5, int var6, java.lang.Object var7) {
if ((var6 & 2) != 0) {
var2 = "p2's default value";
}
if ((var6 & 8) != 0) {
var4 = "p4's default value";
}
var0.aFunctionWithDefaultParameters(var1, var2, var3, var4, var5);
}
每个入参都简单解释一下。
var0
: 一个A
类的实例var1
: 和kotlin
源码中的p1
参数对应var2
: 和kotlin
源码中的p2
参数对应var3
: 和kotlin
源码中的p3
参数对应var4
: 和kotlin
源码中的p4
参数对应var5
: 和kotlin
源码中的p5
参数对应var6
: 用于位运算var7
: 没有用到,我不知道它的作用是什么
其中 var1 ~ var5
分别和 kotlin
源码中的 p1 ~ p5
对应。 我们来看看 var6
的作用。 在 kotlin
源码中,p2
和 p4
有默认值。 如果用户没有提供 p2
参数,那么下图的 if
条件成立,var2
就会被赋成 "p2's default value"
(即 p2
的默认值)

如果用户没有提供 p4
参数,那么下图的 if
条件成立,var4
就会被赋成 "p4's default value"
(即 p4
的默认值)

那么当代码运行到 var0.aFunctionWithDefaultParameters(var1, var2, var3, var4, var5)
那里时,var1 ~ var5
都已经变成了正确的值。
在 kotlin
代码里, aFunctionWithDefaultParameters(p1 = "placeholder for p1", p2 = "placeholder for p2", p3 = "placeholder for p3", p5 = "placeholder for p5")
这个函数调用中,指定了 p4
之外的所有参数,所以只有 p4
需要使用默认值。那么 var6 = 8
就可以让 var4
填上正确的值(即 p4
的默认值 4
)⬇️

基于"步骤一"的推断
由此可以推断,当 kotlin
源码中的某个方法 f(...)
使用了默认参数时(假设入参共有 X
个),编译出的 class
文件中会为这个 f(...)
方法合成一个对应的静态方法 f$default(...)
。 这个 f$default(...)
方法的入参个数会是 X + 3
,如果把它们称为 var0 ~ var(X+2)
的话,那么
var0
: 一个当前类的实例var1 ~ varX
: 分别和f(...)
方法中的X
个参数对应var(X+1)
: 用于位运算var(X+2)
: 不知道是做什么用的
在 kotlin
源码中对 f(...)
方法的调用,在 class
文件中会转化成对 f$default(...)
方法的调用。而 f$default(...)
方法中会借助位运算把所有参数的值都转化好,然后再去调用 m(...)
方法。
一些细节
1. "主要步骤"那张图是如何画出来的?
我在 mermaid.live/ 网站上用如下代码 ⬇️ 可以把主要步骤画出来。有兴趣的读者可以自己尝试尝试。
text
---
config:
layout: dagre
---
flowchart TD
subgraph "主要步骤"
A["`**_kotlin_ 源码的 _callerFunction()_ 里的** _aFunctionWithDefaultParameters(
p1 = "placeholder for p1",
p2 = "placeholder for p2",
p3 = "placeholder for p3",
p5 = "placeholder for p5"
)_`"] -- 转化为 --> B["**_class_ 文件中的** _aFunctionWithDefaultParameters$default(
this,
"placeholder for p1",
"placeholder for p2",
"placeholder for p3",
null,
"placeholder for p5",
8,
null
);_"]
B -- 步骤一 --> C["_aFunctionWithDefaultParameters$default(...)_ 方法中的参数处理"]
B -- 步骤二 --> D["_aFunctionWithDefaultParameters$default(...)_ 方法中调用 _aFunctionWithDefaultParameters(...)_"]
end
2. 用 javap
命令查看 A.class
我用 javap -v -p A
命令查看了 A.class
的内容,具体结果如下(这里只展示了和字段/方法有关的部分,常量池以及 A
类的注解均略去)。
text
{
public A();
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 LA;
public final void aFunctionWithDefaultParameters(java.lang.String, java.lang.String, java.lang.String, java.lang.String, java.lang.String);
descriptor: (Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;)V
flags: (0x0011) ACC_PUBLIC, ACC_FINAL
Code:
stack=2, locals=6, args_size=6
0: aload_1
1: ldc #15 // String p1
3: invokestatic #21 // Method kotlin/jvm/internal/Intrinsics.checkNotNullParameter:(Ljava/lang/Object;Ljava/lang/String;)V
6: aload_2
7: ldc #23 // String p2
9: invokestatic #21 // Method kotlin/jvm/internal/Intrinsics.checkNotNullParameter:(Ljava/lang/Object;Ljava/lang/String;)V
12: aload_3
13: ldc #25 // String p3
15: invokestatic #21 // Method kotlin/jvm/internal/Intrinsics.checkNotNullParameter:(Ljava/lang/Object;Ljava/lang/String;)V
18: aload 4
20: ldc #27 // String p4
22: invokestatic #21 // Method kotlin/jvm/internal/Intrinsics.checkNotNullParameter:(Ljava/lang/Object;Ljava/lang/String;)V
25: aload 5
27: ldc #29 // String p5
29: invokestatic #21 // Method kotlin/jvm/internal/Intrinsics.checkNotNullParameter:(Ljava/lang/Object;Ljava/lang/String;)V
32: return
LineNumberTable:
line 11: 32
LocalVariableTable:
Start Length Slot Name Signature
0 33 0 this LA;
0 33 1 p1 Ljava/lang/String;
0 33 2 p2 Ljava/lang/String;
0 33 3 p3 Ljava/lang/String;
0 33 4 p4 Ljava/lang/String;
0 33 5 p5 Ljava/lang/String;
RuntimeInvisibleParameterAnnotations:
parameter 0:
0: #13()
org.jetbrains.annotations.NotNull
parameter 1:
0: #13()
org.jetbrains.annotations.NotNull
parameter 2:
0: #13()
org.jetbrains.annotations.NotNull
parameter 3:
0: #13()
org.jetbrains.annotations.NotNull
parameter 4:
0: #13()
org.jetbrains.annotations.NotNull
public static void aFunctionWithDefaultParameters$default(A, java.lang.String, java.lang.String, java.lang.String, java.lang.String, java.lang.String, int, java.lang.Object);
descriptor: (LA;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;ILjava/lang/Object;)V
flags: (0x1009) ACC_PUBLIC, ACC_STATIC, ACC_SYNTHETIC
Code:
stack=6, locals=8, args_size=8
0: iload 6
2: iconst_2
3: iand
4: ifeq 10
7: ldc #34 // String p2\'s default value
9: astore_2
10: iload 6
12: bipush 8
14: iand
15: ifeq 22
18: ldc #36 // String p4\'s default value
20: astore 4
22: aload_0
23: aload_1
24: aload_2
25: aload_3
26: aload 4
28: aload 5
30: invokevirtual #38 // Method aFunctionWithDefaultParameters:(Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;)V
33: return
StackMapTable: number_of_entries = 2
frame_type = 10 /* same */
frame_type = 11 /* same */
LineNumberTable:
line 4: 0
line 6: 7
line 4: 10
line 8: 18
line 4: 22
public final void callerFunction();
descriptor: ()V
flags: (0x0011) ACC_PUBLIC, ACC_FINAL
Code:
stack=8, locals=1, args_size=1
0: aload_0
1: ldc #41 // String placeholder for p1
3: ldc #43 // String placeholder for p2
5: ldc #45 // String placeholder for p3
7: aconst_null
8: ldc #47 // String placeholder for p5
10: bipush 8
12: aconst_null
13: invokestatic #49 // Method aFunctionWithDefaultParameters$default:(LA;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;ILjava/lang/Object;)V
16: return
LineNumberTable:
line 17: 0
line 18: 1
line 19: 3
line 20: 5
line 17: 7
line 21: 8
line 17: 10
line 23: 16
LocalVariableTable:
Start Length Slot Name Signature
0 17 0 this LA;
}