Kotlin の enum class
のインスタンスの生成は,呼び出されるたびに行われるのでしょうか,それとも一度生成されて static
なかたちで保持されるのでしょうか.
ドキュメントに載っているかなと思ったのですが特に言及されていないように見えたので bytecode を見てみようと思いました.
用意したコード
次のようなコードを用意しました.
enum class MyEnum(val prop: String) {
Enum1(prop = makeProp("Enum1")),
Enum2(prop = makeProp("Enum2"))
}
class MyClass(val prop: String) {
companion object {
val MyClass1 = MyClass(makeProp("MyClass1"))
}
}
fun makeProp(name: String) : String {
println(">>> makeProp: $name...")
return "Prop of $name"
}
fun fEnumClass() {
println(">> 1: fEnumClass")
val e = MyEnum.Enum1
println(">> prop = ${e.prop}")
}
fun fNewClass() {
println(">> 2: fNewClass")
val c = MyClass(makeProp("MyClass"))
println(">> prop = ${c.prop}")
}
fun fStaticClass() {
println(">> 3: fStaticClass")
val c = MyClass.MyClass1
println(">> prop = ${c.prop}")
}
fun main() {
println("> begin")
fEnumClass()
fEnumClass()
fNewClass()
fNewClass()
fStaticClass()
fStaticClass()
println("> end")
}
-
fEnumClass
はenum class
を参照して使うケース, -
fStaticClass
はcompanion object
でstatic
にインスタンスを持つケースです. -
fNewClass
は関数実行ごとに毎回new
するケースです.
それぞれ 2 回ずつ呼び出すことで、生成処理の makeProp
内の println
の出力が
1 度しか呼び出されないか、それとも毎回呼び出されるか確認してみます。
実行結果
このコードの実行結果は以下のようになります:
> begin
>> 1: fEnumClass
>>> makeProp: Enum1...
>>> makeProp: Enum2...
>> prop = Prop of Enum1
>> 1: fEnumClass
>> prop = Prop of Enum1
>> 2: fNewClass
>>> makeProp: MyClass1...
>>> makeProp: MyClass...
>> prop = Prop of MyClass
>> 2: fNewClass
>>> makeProp: MyClass...
>> prop = Prop of MyClass
>> 3: fStaticClass
>> prop = Prop of MyClass1
>> 3: fStaticClass
>> prop = Prop of MyClass1
> end
こうして見ると,ログの流れから以下の結果がわかります:
-
enum class
:- 初期化は最初に参照された 1 度だけ行われている
- また 参照されていない
Enum2
の初期化も行われている
-
new
:- 初期化は関数での実行ごとに毎回行われている
-
companion object
:- 初期化は
class
が参照されたタイミングで 1 度だけ初期化されている
- 初期化は
ですから、enum class
は companion object
で初期化されるように singleton であると予想できます。
enum class
enum class
の出力する初期化処理の bytecode も見てみます:
public final enum MyEnum extends java/lang/Enum {
// (略)
// access flags 0x8
static <clinit>()V
// (略)
L1
LINENUMBER 3 L1
LDC "Enum2"
INVOKESTATIC MainKt.makeProp (Ljava/lang/String;)Ljava/lang/String;
INVOKESPECIAL MyEnum.<init> (Ljava/lang/String;ILjava/lang/String;)V
DUP
PUTSTATIC MyEnum.Enum2 : LMyEnum;
AASTORE
PUTSTATIC MyEnum.$VALUES : [LMyEnum;
RETURN
MAXSTACK = 9
MAXLOCALS = 0
// (略)
}
L1
あたりの Enum2
を生成する部分に注目しています。PUTSTATIC
が呼び出されています。
// access flags 0x19
public final static fEnumClass()V
// 略
L4
LINENUMBER 19 L4
GETSTATIC MyEnum.Enum1 : LMyEnum;
ASTORE 0
また fEnumClass()
内で MyEnum.Enum1
を参照している部分はこのように GETSTATIC
で参照されます。
companion object
companion object
の初期化の出力する bytecode を見てみます:
public final class MyClass {
// (略)
// access flags 0x8
static <clinit>()V
// (略)
L0
LINENUMBER 8 L0
NEW MyClass
DUP
LDC "MyClass1"
INVOKESTATIC MainKt.makeProp (Ljava/lang/String;)Ljava/lang/String;
INVOKESPECIAL MyClass.<init> (Ljava/lang/String;)V
PUTSTATIC MyClass.MyClass1 : LMyClass;
RETURN
MAXSTACK = 3
MAXLOCALS = 0
// (略)
}
L0
の MyClass1
を生成する部分に似通ったものを感じます。
// access flags 0x1019
public final static synthetic access$getMyClass1$cp()LMyClass;
L0
LINENUMBER 6 L0
GETSTATIC MyClass.MyClass1 : LMyClass;
ARETURN
// (略)
また fStaticClass()
から呼び出される MyClass.MyClass1
を取得する部分も GETSTATIC
が使われています。
通常の new
関数内で new
する場合の初期化処理の bytecode を見てみます:
public final class MyClass {
// access flags 0x1
public <init>(Ljava/lang/String;)V
// annotable parameter count: 1 (visible)
// annotable parameter count: 1 (invisible)
@Lorg/jetbrains/annotations/NotNull;() // invisible, parameter 0
L0
ALOAD 1
LDC "prop"
INVOKESTATIC kotlin/jvm/internal/Intrinsics.checkNotNullParameter (Ljava/lang/Object;Ljava/lang/String;)V
L1
LINENUMBER 6 L1
ALOAD 0
INVOKESPECIAL java/lang/Object.<init> ()V
ALOAD 0
ALOAD 1
PUTFIELD MyClass.prop : Ljava/lang/String;
RETURN
L2
LOCALVARIABLE this LMyClass; L0 L2 0
LOCALVARIABLE prop Ljava/lang/String; L0 L2 1
MAXSTACK = 2
MAXLOCALS = 2
fNewClass
の bytecode もみてみます:
// access flags 0x19
public final static fNewClass()V
L0
LINENUMBER 24 L0
LDC ">> 2: fNewClass"
ASTORE 0
// (略)
L3
L4
LINENUMBER 25 L4
NEW MyClass
DUP
LDC "MyClass"
INVOKESTATIC MainKt.makeProp (Ljava/lang/String;)Ljava/lang/String;
INVOKESPECIAL MyClass.<init> (Ljava/lang/String;)V
ASTORE 0
L5
かなり異なったインストラクションであるように見えます。
全体感
こうして見ると,enum class
と companion object
で保持する場合に出力される bytecode は同じ流れに見え,意味合い的にも <clinit>
で初期化して PUTSTATIC
あたりで static に保存するという部分が同じに思えます (bytecode の知識は全然ないので間違ってたら教えてください!).
なので,enum class
では参照されたときにすべてのメンバーが初期化されて Singleton にインスタンスが保持されるということがわかりました。