CA.ktでJavaに無い機能をKotlinがどう実現してるのか話してきました。

6/15にCyberAgentで開催したCA.kt #1で発表しました。

様子

How Kotlin implements features Java doesn’t have

今回は以下の3つに関して話しました。

  • NonNull/Nullable
  • Extension Function
  • Named Argument/Default Argument

ブログ向けに改めて書き起こします。サンプルコードはKotlinとそのBytecodeをデコンパイルして生成したJavaです。

NonNull/Nullable

関数の引数を例に解説しました。この場合関数がpublicなのかprivateなのか引数がJavaでいうPrimitive型なのかClass型なのかでコンパイル後のBytecodeが行っている処理に変化が置きます。

引数がNonNull-Primitive型

fun nonNullInt(number: Int) {
  number.toString()
}
public final void nonNullInt(int number) {
   String.valueOf(number);
}

これらはもともとnullを許容しないのでそのままコンパイルされます。

  • byte
  • short
  • int
  • long
  • char
  • float
  • double
  • boolean

引数がNullable-Primitive型

fun nullableInt(number: Int?) {
  number?.toString()
}
public final void nullableInt(@Nullable Integer number) {
   if(number != null) {
      String.valueOf(number.intValue());
   }
}

nullableの場合、intのBoxTypeであるIntegerにへコンパイルされます。Integerであればnullを許容するからです。
number?.toString()といったSafe Callはif文でnullチェックを行って実行されます。

引数がNonNull-Class型

fun nonNullString(string: String) {
  string.length
}
public final void nonNullString(@NotNull String string) {
   Intrinsics.checkParameterIsNotNull(string, "string");
   string.length();
}

StringなどのClass型は本来nullを許容する型なので、関数の最初にIntrinsics#checkParameterIsNotNull(Kotlinのruntimeが持っている関数)で引数のnullチェックを行います。JavaなどのNullable/NonNullの区別がない言語からnullを渡される可能性があるからです。

引数がNullable-Class型

private fun nullableString(string: String?) {
  string?.length
}
private final void nullableString(String string) {
   if(string != null) {
      string.length();
   }
}

今度は関数先頭でのIntrinsics.checkParameterIsNotNullの呼び出しがなくなりました。引数のnullのチェックをする必要が無いからです。

Private関数

引数がNonNull-Class型の項でJavaからnullが渡される可能性を考えて、nullチェックを行っていると解説しました。
しかし、この関数をPrivateにするとまた変化が起こります。

private fun nonNullString(string: String) {
  string.length
}
private final void nonNullString(String string) {
   string.length();
}

引数に対する@NotNullアノテーションとIntrinsics.checkParameterIsNotNullの呼び出しがなくなりました。Private関数であればKotlin以外の言語から呼び出されない事が保証されるからです(Reflectionは例外ですが)。

Extension Function

Package Level Extension Function

StringExtension.ktファイルの直下に以下のような拡張関数を実装してみます。

fun String.println() {
    println(this)
}
public final class StringExtensionKt {
   public static final void println(@NotNull String $receiver) {
      Intrinsics.checkParameterIsNotNull($receiver, "$receiver");
      System.out.println($receiver);
   }
}

FileNameKtクラスのstatic methodとしてコンパイルされます。引数は拡張対象です。
そして、このExtension Functionの呼び出し元はこのstatic methodを呼ぶようにコンパイルされます。

Class Level Extension Function

class Foo {
  fun String.println() {
    println(this)
  }
}
public final class Foo {
  public final void println(@NotNull String $receiver) {
  Intrinsics.checkParameterIsNotNull($receiver, "$receiver");
  System.out.println($receiver);
  }
}

この場合はFooクラスのmethodとしてコンパイルされます。

Private Package Level Extension Function

Private Package Level Extension Functionは同ファイル内のみからアクセス可能なExtension Functionです。もちろんこちらもPackage Levelなのでstatic methodとしてコンパイルされます。しかし、そのstatic methodの公開範囲はprivateです。

以下のようなコードをFoo.ktに実装した場合について考えてみましょう。

private fun Foo.doSomething() {
}

class Foo {
  init {
    doSomething()
  }
}

Foo#doSomethingはPrivate Package Level Extension Functionなので、FooKtクラスprivate static methodとして実装されます。 FooKtのprivateなメソッドにFooからアクセスできるのはおかしいですね。
そこで、Bytecodeを見てみると以下のようになっています。

public final static synthetic access$doSomething(LFoo;)V
    @Lorg/jetbrains/annotations/NotNull;() // invisible, parameter 0
   L0
    LINENUMBER 1 L0
    ALOAD 0
    INVOKESTATIC FooKt.doSomething (LFoo;)V
    RETURN
   L1
    LOCALVARIABLE $receiver LFoo; L0 L1 0
    MAXSTACK = 1
    MAXLOCALS = 1
  ...

Fooを引数とするpublic final static synthetic access$doSomethingというsynthetic methodが生成されています。 これはコンパイラが自動生成したメソッドです。これを呼び出し元をこのメソッドを経由するようにすることでFooからFooKtのprivate static methodにアクセスします。

余談ですが、synthethic methodはJavaでNested Classを実装したときにも生成されます。Outer classInner class間でprivateなメソッドまたはフィールドにアクセスしようとした時です。JavaにはNested Classの仕様がありますが、Bytecode上は別々のクラスとしてコンパイルされるからです。

Named Argument

fun foo(one: String, two: String, three: String) {
}

foo(three = "tres", two = "dos", one = "uno")
String var1 = "uno";
String var2 = "dos";
String var3 = "tres";
this.foo(var1, var2, var3);

Named Argumentは一度変数に代入してから順番を揃えて関数を呼び出すようにコンパイルされます。

Default Argument

fun foo(one: String = one",
        two: String = two",
        three: String = "three") {
}

foo(two = "dos")
// staticなのはfoo関数をパッケージレベルに実装してコンパイルしたため。
public static void foo$default(
    String var1, String var2, String var3, int var4, Object var5) {
   if((var4 & 1) != 0) {
      var1 = "one";
   }
   if((var4 & 2) != 0) {
      var2 = "two";
   }
   if((var4 & 4) != 0) {
      var3 = "three";
   }
   foo(var1, var2, var3);
}

foo$default((String)null, "dos", (String)null, 5, (Object)null);

Default ArgumentfunctionName$defaultというメソッドが生成され、その中から関数を呼ぶようにコンパイルされます。 この例では、第1引数〜第3引数は元の関数と一致し、第4引数はDefault Argumentを適用するべき位置をこの$defaultメソッドに教えるための設定値、第5引数のObjectは用途不明です(誰か教えてくれ)。
Default Argumentを適用すべき箇所を1すべきでない箇所を0と表現します。
すると、foo$default((String)null, “dos”, (String)null, 5, (Object)null)1, 0, 1となります。101を10進数に治すと5になります。これを第4引数に代入しています。 $defaultメソッドの中では101001, 010, 100の論理積を0と比較することでDefault Argumentを実際に適用するかどうかを決定しています。

こういった知識は必要なのか

上記はほんの一部に過ぎないが、KotlinはこのようにしてJavaが持たない機能を実現している。私達がよくある無駄なコードを書く手間をコンパイラが肩代わりしてくれています。
この知識が必要かといわれると、基本的には必要ないと思う。しかし、Javaとの相互運用を行う際にKotlinのコードがJavaからどうみえるのかといったことが分かると無駄にハマることが減ると思う。
たとえば、AndroidのDataBindingBindingAdapterstatic methodを利用する。これをそのままKotlinでcompanion objectで実装しようとしてもうまくいかない。companion objectはそのクラスのinner class Companionとしてコンパイルされ、そのクラスのstatic fieldとなるからです。
そこで、Javaからどうみえるかを意識できていれば「@JvmStaticアノテーションを使おう」だとか「Extension Functionとして実装しよう」といったことが思いつきます。


Kotlin