Kotlin 中的默认参数在 class 文件中是如何实现的?

Kotlin 中的默认参数在 class 文件中是如何实现的?

kotlin 的方法中可以使用默认参数,那么这一特性在 class 文件中是如何做到的呢?本文对此进行介绍。

结论

  1. 会有一个静态合成方法负责将用户未提供的参数替换为对应的默认值
  2. 这个替换过程用到了位运算

代码

我们用以下的 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 个方法

  1. 构造函数
  2. aFunctionWithDefaultParameters(...) 方法
  3. aFunctionWithDefaultParameters$default(...) ⬅️ 是静态方法
  4. 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 源码中,p2p4 有默认值。 如果用户没有提供 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;
}
相关推荐
xjdkxnhcoskxbco2 天前
kotlin基础【1】
java·前端·kotlin
居然是阿宋2 天前
Kotlin Flow 实战:StateFlow 和 SharedFlow 的默认值陷阱
android·开发语言·kotlin
xjdkxnhcoskxbco2 天前
kotlin基础【2】
android·开发语言·kotlin
AI大法师2 天前
Kotlin与Jetpack Compose:Android开发生态的演进与架构思考
android·kotlin
Harry技术2 天前
ArcSoft 裁剪错误修复方案
android·kotlin
说码解字2 天前
Android MediaCodec 的使用和源码实现分析
android·开发语言·kotlin
涵涵子RUSH2 天前
android studio(NewsApiDemo)100%kotlin
android·kotlin·android studio
安卓开发者3 天前
Android KTX:让Kotlin开发更简洁高效的利器
android·开发语言·kotlin