Kotlin的默认参数是如何工作的?

刚刚开始接触Kotlin,尤其是涉及Java-Kotlin混合开发的人,对于注解JvmOverloads,一定不陌生。今天的主题,就是这个货。

JvmOverloads

首先,来看看注解JvmOverloads的注释

Instructs the Kotlin compiler to generate overloads for this function that substitute default parameter values. If a method has N parameters and M of which have default values, M overloads are generated: the first one takes N-1 parameters (all but the last one that takes a default value), the second takes N-2 parameters, and so on.

要点

  1. 注解的作用:告知编译器产生重载方法,以消除默认值参数方法的存在 —— 即以多个重载方法实现“默认参数值”
  2. 如果一个方法有N个参数,其中M个有默认值。那么会产生M个重载方法
  3. 上述M个方法:

    • 第1个方法:有N-1个参数,最后1个参数使用默认值
    • 第2个方法:有N-2个参数,最后2个参数使用默认值
    • 第M个方法:有N-M个参数,最后M个参数使用默认值

也就是说,带默认值参数的kotlin方法转为java代码后,则变成了多个重载方法。

那么,这个转换过程是怎么样的呢?是什么原理?

原理

一段测试代码如下:

1
2
3
4
5
6
7
8
class Test {

    @JvmOverloads
    fun testParam(s: String, count: Int = 4, flag: Boolean = true, tip: String = "hello") {
        println("$s -- $count, $flag, $tip")
    }

}

测试类Test定义了一个方法testParam,该方法共4个参数,后3个参数各带了一个默认值。

来看看上述代码生成的java代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
public final class Test {
   @JvmOverloads
   public final void testParam(@NotNull String s, int count, boolean flag, @NotNull String tip) {
      Intrinsics.checkNotNullParameter(s, "s");
      Intrinsics.checkNotNullParameter(tip, "tip");
      String var5 = s + " -- " + count + ", " + flag + ", " + tip;
      boolean var6 = false;
      System.out.println(var5);
   }

   // $FF: synthetic method
   public static void testParam$default(Test var0, String var1, int var2, boolean var3, String var4, int var5, Object var6) {
      if ((var5 & 2) != 0) {
         var2 = 4;
      }

      if ((var5 & 4) != 0) {
         var3 = true;
      }

      if ((var5 & 8) != 0) {
         var4 = "hello";
      }

      var0.testParam(var1, var2, var3, var4);
   }

   // 以下三个方法,为生成的重载方法

   // 方法A
   @JvmOverloads
   public final void testParam(@NotNull String s, int count, boolean flag) {
      testParam$default(this, s, count, flag, (String)null, 8, (Object)null);
   }

   // 方法B
   @JvmOverloads
   public final void testParam(@NotNull String s, int count) {
      testParam$default(this, s, count, false, (String)null, 12, (Object)null);
   }

   // 方法C
   @JvmOverloads
   public final void testParam(@NotNull String s) {
      testParam$default(this, s, 0, false, (String)null, 14, (Object)null);
   }
}

总共生成了3个重载方法,与带默认值的参数的数量一致。这些重载方法是怎么做到“默认参数值功能”的呢?

先来看看静态合成方法testParam$default,它有7个参数:

  • var0:对象this引用
  • var1~var4:分别对应四个参数
  • var5:一个标志值(什么用处呢?)
  • var6:未使用

而三个重载方法都直接调用了上述合成方法,传入了不同参数。

首先来看看全部使用默认值的方法C。原始声明里,三个默认值依次是:4, true, “hello”,而这里传入合成方法的是:0, false, null。var5传入了14,var6传入null。

很费解,这里14是什么意思?为什么全部使用默认值的方法,反而一个默认值都没传进去?再回头来看看合成方法就明白了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
      // 14 & 2不为0,所以满足,var2变为4(就是默认值)
      if ((var5 & 2) != 0) {
         var2 = 4;
      }

      // 14 & 4不为0,所以满足,var3变为true(就是默认值)
      if ((var5 & 4) != 0) {
         var3 = true;
      }

      // 14 & 8不为0,所以满足,var4变为"hello"(就是默认值)
      if ((var5 & 8) != 0) {
         var4 = "hello";
      }

      // 用最终值调用全参方法
      var0.testParam(var1, var2, var3, var4);

看完上面的代码和注释,是不是原理呼之欲出?

参数序列,用一个整型由低位到高位依次标志,0代表没有默认值,1代表有默认值;不同的重载方法使用不同的标志位,来确定哪些参数将使用默认值。

方法C传入的14,即0b1110,var2~var4的标志分别为: 0b10, 0b100, 0b1000,所以14即代表,这三个参数都使用默认值。

再来分析方法B来验证上述结论:var3和var4使用默认值,传入无效参数false和null,标志位是12(即0b1100),自然,调用合成方法,只有高位两位满足,即var3和var4使用默认值。结论正确。

小结

默认参数方法到重载方法,巧妙地使用了“按位取值”,把默认参数值与重载方法一一对应起来了。