読者です 読者をやめる 読者になる 読者になる

Kotlin拡張関数は怖くない、その実態を紐解く。

Kotlin未経験Javaエンジニアに拡張関数を説明すると「怖い」と言われることがあります。 おそらく、クラスを継承する事なく拡張できることが黒魔術的に見えるがゆえの感想なのではないでしょうか。 本稿では拡張関数の実態を知ることで、拡張関数をもっと身近に感じてもらいたいと思います。

拡張関数とはなにか

拡張関数はクラスを継承せずに機能を追加するための機能です。 下記のようにfun Type.functionName(...)とすることでそのTypeに新たに関数を追加する事ができます。

fun String.println() {
    println(this)
}

拡張関数は通常の関数と同じように呼び出すことが可能です。

"Hello World".println() // out: Hello World

拡張関数はどのように実現されているのか

StringExtension.ktファイルに実装した下記コードをコンパイルすると、StringExtensionKt.classが生成されます。

fun String.println() {
    println(this)
}

ではStringExtensionKt.classJavaデコンパイルしてみましょう。

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

ファイル名Ktクラスに拡張関数と同名のstaticメソッドが生成されていることから、拡張関数がただのstaticメソッドであることがわかると思います。 ここまで分かってしまえば何も怖いものはありませんね。

private拡張関数

以下のようなFooクラスがあるとします。

class Foo {

}

これに対してFooExtensionファイルにprivate拡張関数を実装しましょう。

private fun Foo.doSomething() {
    
}

private関数ですので、Foo内からであれば呼び出せることは当たり前ですね。

class Foo {
    init {
        doSomething()
    }
}

さて、ここで一つ疑問が出てきました。先程の例と同様であれば以下のようにFooExtensionKtクラスにprivate staticメソッドが生成されるはずです。 となるとFooExtensionのprivate staticメソッドをFooから呼び出せる事はおかしいですね。

public final class FooExtensionKt {
   private static final void doSomething(@NotNull Foo $receiver) {
   }
}

実際にコンパイルしてみると以下のようになりました。

public final class FooExtensionKt {
   private static final void doSomething(@NotNull Foo $receiver) {
   }

   public static final void access$doSomething(@NotNull Foo $receiver) {
      doSomething($receiver);
   }
}

doSomethingへアクセスするためのブリッジメソッドが生成されています。 ここでFoo.classもデコンパイルしてみましょう。

public final class Foo {
   public Foo() {
      FooExtensionKt.access$doSomething(this);
   }
}

なんと、private拡張関数を呼び出していた部分はブリッジメソッドの呼び出しに変わっています。 どうやらコンパイル時にブリッジメソッドが生成され、Kotlinからのprivate拡張関数呼び出しはこのブリッジメソッド呼び出しに置き換えられるようです。

では、もしFooがJavaクラスだった場合はどうでしょうか。 この場合そのprivateメソッドはまったくの役立たずになります。Javaからはただのstaticメソッドとしか見えない上にprivateであればアクセスする方法が無いからです。 コンパイル時に生成されるブリッジメソッドへのアクセスもコンパイル前では不可能です。

おわりに

どうでもいいですが、今回はバイトコードもそこそこ読みました。 普段全く読むことがないのでバイトコードを読む力はあまりないですが、これを機にちょくちょく見ていこうかなと思います。 Android StudioであればTools -> Kotlin -> Show Kotlin Bytecodeで即座に読むことが出来ますので、是非活用してみてください。