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

136 阅读7分钟

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

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

结论

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

代码

我们用以下的 kotlin 代码来进行探索(请将代码保存为 A.kt

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.class 文件。用javap -p A 命令可以查看 A.class 中的简要内容。 结果如下

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() 方法

我们用 Intellij IDEAShow Kotlin Bytecode 功能可以查看 A.class 的内容。结果如下 ⬇️

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

@Metadata(
   mv = {2, 2, 0},
   k = 1,
   xi = 48,
   d1 = {"\u0000\u001a\n\u0002\u0018\u0002\n\u0002\u0010\u0000\n\u0002\b\u0003\n\u0002\u0010\u0002\n\u0000\n\u0002\u0010\u000e\n\u0002\b\u0005\u0018\u00002\u00020\u0001B\u0007¢\u0006\u0004\b\u0002\u0010\u0003J2\u0010\u0004\u001a\u00020\u00052\u0006\u0010\u0006\u001a\u00020\u00072\b\b\u0002\u0010\b\u001a\u00020\u00072\u0006\u0010\t\u001a\u00020\u00072\b\b\u0002\u0010\n\u001a\u00020\u00072\u0006\u0010\u000b\u001a\u00020\u0007J\u0006\u0010\f\u001a\u00020\u0005"},
   d2 = {"LA;", "", "<init>", "()V", "aFunctionWithDefaultParameters", "", "p1", "", "p2", "p3", "p4", "p5", "callerFunction"}
)
public final class A {
   public final void aFunctionWithDefaultParameters(@NotNull String p1, @NotNull String p2, @NotNull String p3, @NotNull String p4, @NotNull String p5) {
      Intrinsics.checkNotNullParameter(p1, "p1");
      Intrinsics.checkNotNullParameter(p2, "p2");
      Intrinsics.checkNotNullParameter(p3, "p3");
      Intrinsics.checkNotNullParameter(p4, "p4");
      Intrinsics.checkNotNullParameter(p5, "p5");
   }

   // $FF: synthetic method
   public static void aFunctionWithDefaultParameters$default(A var0, String var1, String var2, String var3, String var4, String var5, int var6, 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);
   }

   public final void callerFunction() {
      aFunctionWithDefaultParameters$default(this, "placeholder for p1", "placeholder for p2", "placeholder for p3", (String)null, "placeholder for p5", 8, (Object)null);
   }
}

调用 callerFunction() 时,涉及的主要步骤如下 ⬇️

image.png

复杂的地方在 步骤一,它会处理参数默认值的逻辑。我们看一下这一步的细节

“步骤一” 的分析

aFunctionWithDefaultParameters$default(...) 对应的 java 代码如下 ⬇️

   public static void aFunctionWithDefaultParameters$default(A var0, String var1, String var2, String var3, String var4, String var5, int var6, 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 的默认值)

image.png

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

image.png

那么当代码运行到 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 的默认值 "p4's default value")⬇️

image.png

至于为何会选用 28 来进行位运算,我觉得是和参数的位置有关 ⬇️

image.png

如果默认参数很多(超过 32 个)怎么办?

有的朋友可能会问,既然这个用来做位运算的参数是 java 里的 int,那它只有 32bit,如果 kotlin 代码里的方法 f(...) 用到了超过 32 个默认参数怎么办? 我试了几次,基于测试的结果,我的推测是如果 kotlin 里的 f(...)N 个默认参数,那么 class 文件里的 f$default(...) 方法中会有 (N + 31)/32 个用于位运算的 int 参数。

  • N >= 1 && N <= 32: 需要 1 个这样的参数
  • N >= 33 && N <= 64: 需要 2 个这样的参数
  • N >= 65 && N <= 96: 需要 3 个这样的参数
  • ...

基于“步骤一”的推测

基于上文的分析,可以推测,当 kotlin 源码中的某个方法 f(...) 使用了默认参数时(假设入参共有 X 个,且默认参数不超过 32 个),编译出的 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(...) 方法中会借助位运算把所有参数的值都转化好,然后再去调用 f(...) 方法。

image.png

如何从 java 代码中调用?

基于上文的分析,我们知道在 kotlin 代码中,如果 f(...) 方法使用了默认参数,那么在 class 文件中,会把对 f(...) 方法的调用转化为对 f$default(...) 方法的调用。但是对 java 代码的调用方来说,f$default(...) 用起来并不方便。@JvmOverloads 注解可以解决这个问题。 我们来看一下修改后的 A.kt ⬇️ (变化是 aFunctionWithDefaultParameters(...) 方法多了 @JvmOverloads 注解)

class A {
    // 此方法有 5 个参数 ➡️  p1/p2/p3/p4/p5
    // 只有 p2/p4 有默认值
    @JvmOverloads
    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"
        )
    }
}

我们仍旧用 Intellij IDEAShow Kotlin Bytecode 功能来查看 A.class 的内容。结果如下 ⬇️

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

@Metadata(
   mv = {2, 2, 0},
   k = 1,
   xi = 48,
   d1 = {"\u0000\u001a\n\u0002\u0018\u0002\n\u0002\u0010\u0000\n\u0002\b\u0003\n\u0002\u0010\u0002\n\u0000\n\u0002\u0010\u000e\n\u0002\b\u0005\u0018\u00002\u00020\u0001B\u0007¢\u0006\u0004\b\u0002\u0010\u0003J4\u0010\u0004\u001a\u00020\u00052\u0006\u0010\u0006\u001a\u00020\u00072\b\b\u0002\u0010\b\u001a\u00020\u00072\u0006\u0010\t\u001a\u00020\u00072\b\b\u0002\u0010\n\u001a\u00020\u00072\u0006\u0010\u000b\u001a\u00020\u0007H\u0007J\u0006\u0010\f\u001a\u00020\u0005"},
   d2 = {"LA;", "", "<init>", "()V", "aFunctionWithDefaultParameters", "", "p1", "", "p2", "p3", "p4", "p5", "callerFunction"}
)
public final class A {
   @JvmOverloads
   public final void aFunctionWithDefaultParameters(@NotNull String p1, @NotNull String p2, @NotNull String p3, @NotNull String p4, @NotNull String p5) {
      Intrinsics.checkNotNullParameter(p1, "p1");
      Intrinsics.checkNotNullParameter(p2, "p2");
      Intrinsics.checkNotNullParameter(p3, "p3");
      Intrinsics.checkNotNullParameter(p4, "p4");
      Intrinsics.checkNotNullParameter(p5, "p5");
   }

   // $FF: synthetic method
   public static void aFunctionWithDefaultParameters$default(A var0, String var1, String var2, String var3, String var4, String var5, int var6, 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);
   }

   public final void callerFunction() {
      aFunctionWithDefaultParameters$default(this, "placeholder for p1", "placeholder for p2", "placeholder for p3", (String)null, "placeholder for p5", 8, (Object)null);
   }

   @JvmOverloads
   public final void aFunctionWithDefaultParameters(@NotNull String p1, @NotNull String p2, @NotNull String p3, @NotNull String p5) {
      Intrinsics.checkNotNullParameter(p1, "p1");
      Intrinsics.checkNotNullParameter(p2, "p2");
      Intrinsics.checkNotNullParameter(p3, "p3");
      Intrinsics.checkNotNullParameter(p5, "p5");
      aFunctionWithDefaultParameters$default(this, p1, p2, p3, (String)null, p5, 8, (Object)null);
   }

   @JvmOverloads
   public final void aFunctionWithDefaultParameters(@NotNull String p1, @NotNull String p3, @NotNull String p5) {
      Intrinsics.checkNotNullParameter(p1, "p1");
      Intrinsics.checkNotNullParameter(p3, "p3");
      Intrinsics.checkNotNullParameter(p5, "p5");
      aFunctionWithDefaultParameters$default(this, p1, (String)null, p3, (String)null, p5, 10, (Object)null);
   }
}

和之前的结果相比,现在多了两个重载方法

  • aFunctionWithDefaultParameters(String, String, String, String) ⬅️ 如果 java 代码的调用方提供 p2 参数但不提供 p4 参数,则调用此方法
  • aFunctionWithDefaultParameters(String, String, String) ⬅️ 如果 java 代码的调用方既不提供 p2 参数也不提供 p4 参数,则调用此方法

这样,java 代码的调用方只需选择合适的 aFunctionWithDefaultParameters(...) 方法来调用即可。当然这两个重载方法最终还是会调用 aFunctionWithDefaultParameters$default(...) 方法,但是 java 代码的调用方就不用关心这些位运算的细节了。

其他

文中的两张图是如何画出来的?

两张图都是借助了 mermaid.live/ 网站的帮助而画出来的。用到的代码如下

第一张图
---
config:
  layout: dagre
---
flowchart TD
    subgraph "主要步骤"
    A["`**_kotlin_ 源码的 _callerFunction()_ 里的** _aFunctionWithDefaultParameters(
      p1 = &quot;placeholder for p1&quot;,
      p2 = &quot;placeholder for p2&quot;,
      p3 = &quot;placeholder for p3&quot;,
      p5 = &quot;placeholder for p5&quot;
)_`"] -- 转化为 --> B["**_class_ 文件中的** _aFunctionWithDefaultParameters$default(
    this, 
    &quot;placeholder for p1&quot;,
    &quot;placeholder for p2&quot;,
    &quot;placeholder for p3&quot;,
    null,
    &quot;placeholder for p5&quot;,
    8,
    null
);_"]
    B -- 步骤一 --> C["_aFunctionWithDefaultParameters$default(...)_ 方法中的参数处理"]:::important
    B -- 步骤二 --> D["_aFunctionWithDefaultParameters$default(...)_ 方法中调用 _aFunctionWithDefaultParameters(...)_"]
    end


classDef important stroke:#f00
第二张图
---
config:
  layout: dagre
---
flowchart LR
 subgraph s1["In _kotlin_ source code"]
        A["caller function calls callee function _f(...)_ **(callee function has default parameters)**"]
  end
 subgraph s2["In _class_ file"]
        Y["_f$default(...)_ populates each parameter that needs default value"]
        X["caller function calls _f$default(...)_"]
        Z["_f$default(...)_ calls _f(...)_ with proper parameters"]
  end
    X --> Y
    Y --> Z
    s1 -- 转化为 --> s2