久々にKotlinを触ったのでメモしておく。
[初期化ブロックとの再会]
KotestというKotlinのテストライブラリがある*1。このリポジトリに公式サイトを写経しながら使い方や機能の理解に努めている。
Kotestでは一番シンプルな書き方だと、以下のようにテストを書く。
class MyTests : StringSpec({
"length should return size of string" {
"hello".length shouldBe 5
}
})
JUnitではテストコードをテストクラスの中に記述する。一方Kotestでは、上記のようにXXXSpecクラスを継承し、そのコンストラクタにラムダ式としてテストコードを渡す。
StringSpecクラスは以下のように、引数なし、戻り値なしの関数を引数にとる抽象クラスとして実装されている。
abstract class StringSpec(
body: StringSpec.() -> Unit = {}
) : DslDrivenSpec(), StringSpecRootScope {
init {
body()
}
}
kotest/stringSpec.kt at 38d123c6311b24d1174a6313363f96564e17ebcf · kotest/kotest · GitHub
ところで、Kotestのテストコードは、このページにあるように、初期化ブロック(initブロック)に記述することもできる。
class MyTests : StringSpec() {
init {
// tests here
}
}
Kotlinを真面目に触るのは半年ぶりくらいなので、初期化ブロックなんてあったっけ...?と思っていたが、どうやら一年半前の勉強会で出会っていたらしい。
[初期化ブロックの挙動]
Kotlinのドキュメントによれば、Kotlinのプライマリコンストラクタにはロジックを書くことができない。
セカンダリコンストラクタであればロジックを書くことができる。
class Pet { // プライマリコンストラクタ(引数なし)
constructor(owner: Person) { // セカンダリコンストラクタ
owner.pets.add(this)
}
}
プライマリコンストラクタでは、代わりに初期化ブロックが利用できるようだ。
以下の例のように、間にプロパティを初期化するコードが入っていても、initブロックと区別なく、書かれた順に上から実行される。
class InitOrderDemo(name: String) {
val firstProperty = "First property: $name".also(::println)
init {
println("First initializer block that prints $name")
}
val secondProperty = "Second property: ${name.length}".also(::println)
init {
println("Second initializer block that prints ${name.length}")
}
}
fun main() {
InitOrderDemo("hello")
}
// 実行すると以下が出力される
First property: hello
First initializer block that prints hello
Second property: 5
Second initializer block that prints 5
[デコンパイル結果]
そういえば、Javaにも初期化ブロックとかstatic初期化ブロックがあったのを思い出す。
先ほどのInitOrderDemo.ktを、JVMでの実行用にクラスファイル(.class)にコンパイルし、JD-GUI(Java Decompiler)を利用してデコンパイルしてみた。
Gradleは7.4、Kotlinは1.5.31を利用している。
結果は以下の通り。
import kotlin.Metadata; import kotlin.Unit; import kotlin.jvm.internal.Intrinsics; import org.jetbrains.annotations.NotNull; @Metadata(/** 省略 */) public final class InitOrderDemo { @NotNull private final String name; @NotNull private final String firstProperty; @NotNull private final String secondProperty; public InitOrderDemo(@NotNull String name) { this.name = name; String str1 = Intrinsics.stringPlus("First property: ", this.name); boolean bool1 = false, bool2 = false; String str2 = str1; InitOrderDemo initOrderDemo = this; int $i$a$-also-InitOrderDemo$firstProperty$1 = 0; boolean bool3 = false; System.out.println(str2); Unit unit = Unit.INSTANCE; initOrderDemo.firstProperty = str1; str1 = Intrinsics.stringPlus("First initializer block that prints ", this.name); bool1 = false; System.out.println(str1); str1 = Intrinsics.stringPlus("Second property: ", Integer.valueOf(this.name.length())); bool1 = false; bool2 = false; Object p0 = str1; initOrderDemo = this; int $i$a$-also-InitOrderDemo$secondProperty$1 = 0; bool3 = false; System.out.println(p0); unit = Unit.INSTANCE; initOrderDemo.secondProperty = str1; str1 = Intrinsics.stringPlus("Second initializer block that prints ", Integer.valueOf(this.name.length())); bool1 = false; System.out.println(str1); } // あとはgetterなので省略 }
思ったより長いコードになっていてやや驚いたが、初期化ブロックに書いた処理も、プロパティ初期化処理も、全部まとめてコンストラクタの中で実行されるということが分かった。
デコンパイルされたコードをよく見ると、Kotlinのprintln()がJavaのSystem.out.println()になっているのは予想通りとしても、テンプレート文字列がIntrinsics.stringPlus()というメソッドで処理されていたり、KotlinのString型(nullでない) が@NotNullアノテーション付きのJavaのString型になっていたりと面白い。
途中に大量にあるint型、Unit型、boolean型の謎の変数は一体何なのだろう...特に使われていないようだが。
ちなみに、
class MyTests : StringSpec() {
init {
"length should return size of string" {
"hello".length shouldBe 5
}
}
}
というテストコードはMyTests.classとMyTests$1.class(MyTestsの内部クラス)にコンパイルされた。デコンパイルした結果は冗長なので省略するが、MyTests$1.classにテストコード本体が含まれており、MyTests.classがそちらを呼び出す形になっていた。
同じKotlinの初期化ブロックでも、継承が絡むからなのかクラスファイルへのコンパイル結果が変わっていて面白い。
[まとめ]
- Kotlinのプライマリコンストラクタにロジックが書けないので初期化ブロック(init { })が使える
- シンプルな初期化ブロックはクラスファイルへのコンパイル後コンストラクタで実行される
- Kotlinをクラスファイルにコンパイルすると謎の変数が追加されていた