6/15にCyberAgentで開催したCA.kt #1で発表しました。
様子
privateだとkotlinからしか使われないからbytecodeがpublicと違うのか #ca_kt
— すたぜろ (@STAR_ZERO) June 15, 2017
デフォルト引数がJavaに変換されたとき面白い!元の引数に加えてどこにどの引数があるかを表すビットをさらに取って論理積を取って場所を判断するのか!各ビットが各引数の位置というわけね #ca_kt
— しろやま (@fushiroyama) June 15, 2017
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 class
とInner 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 Argument
はfunctionName$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
メソッドの中では101
と001
, 010
, 100
の論理積を0
と比較することでDefault Argumentを実際に適用するかどうかを決定しています。
こういった知識は必要なのか
上記はほんの一部に過ぎないが、KotlinはこのようにしてJavaが持たない機能を実現している。私達がよくある無駄なコードを書く手間をコンパイラが肩代わりしてくれています。
この知識が必要かといわれると、基本的には必要ないと思う。しかし、Javaとの相互運用を行う際にKotlinのコードがJavaからどうみえるのかといったことが分かると無駄にハマることが減ると思う。
たとえば、AndroidのDataBinding
のBindingAdapter
はstatic method
を利用する。これをそのままKotlinでcompanion object
で実装しようとしてもうまくいかない。companion object
はそのクラスのinner class Companion
としてコンパイルされ、そのクラスのstatic fieldとなるからです。
そこで、Javaからどうみえるかを意識できていれば「@JvmStatic
アノテーションを使おう」だとか「Extension Function
として実装しよう」といったことが思いつきます。