前回は、コルーチンについてをまとめました。
今回は初期化についてをまとめておきます。
初期化の順序
突然ですが、まずは各初期化処理がどの順序で呼ばれるのかを把握しておきます。
class User(val name: String, val age: Int) { val country = "Japan" var town = "Tokyo" var active = false init { println("initialize...") active = true } constructor(_name: String): this(_name, 0) { town = "Osaka" } }
このままではどの順序になるか不明ですが、バイトコードからJavaに変換した結果を見るとおおよそ把握できます。
// ... 中略 ... public final class User { @NotNull private final String country; @NotNull private String town; private boolean active; @NotNull private final String name; private final int age; // ... 中略 ... public User(@NotNull String name, int age) { Intrinsics.checkNotNullParameter(name, "name"); super(); this.name = name; this.age = age; this.country = "Japan"; this.town = "Tokyo"; String var3 = "initialize..."; boolean var4 = false; System.out.println(var3); this.active = true; } public User(@NotNull String _name) { Intrinsics.checkNotNullParameter(_name, "_name"); this(_name, 0); this.town = "Osaka"; } }
このコードを見る限りでは以下の順序になっています。
これをもとに各初期化についてをまとめていきます。
コンストラクタ
まずはコンストラクタを見ていきます。
Kotlinでは2つのコンストラクタがあり、それぞれプライマリコンストラクタ、セカンダリコンストラクタと呼ばれます。
プライマリコンストラクタ
プライマリコンストラクタは、インスタンスの初期化に利用する値を引数として受け取り、インスタンスの初期化をすることができます。
class User(_name: String, _age: Int) { val name: String = _name val age: Int = _age }
単に引数として受け取ることもできますが、規定のgetter, setter を利用する場合は、プライマリコンストラクタで定義までできます。
class User(val name: String, val age: Int) { }
セカンダリコンストラクタ
セカンダリコンストラクタを利用すると、プライマリコンストラクタとは別の構築方法を提供することができます。
class User(val name: String, val age: Int) { constructor(_name: String) : this(_name, 0) }
セカンダリコンストラクタはクラス定義で constructor()
で定義します。
セカンダリコンストラクタは、constructor(...) : this(...)
のようにして、プライマリコンストラクタか、他のセカンダリコンストラクタを呼び出す必要があります。
セカンダリコンストラクタは初期化ロジックを別途持つことができます。
class User(val name: String, val age: Int) { constructor(_name: String) : this(_name, 0) { println("Call secondary constructor!") } }
初期化ブロック
Kotlinでは、コンストラクタ以外にもクラス定義で初期化を行うことができます。その1つが初期化ブロックです。
class User(val name: String, val age: Int) { init { println("Call init block!") require(age >= 0, { "age must be positive!" }) } }
プロパティの初期化
ここまでに紹介した以外に、クラス定義でプロパティを初期化することもできます。
import java.util.Date class User(val name: String, val age: Int) { val createdDate: Date = Date() }
プロパティの初期化の際にはクラスに定義された関数を呼び出すこともできます。
遅延される初期化
Kotlinでは null
を許容するかどうかを厳密に判断するため、最初にオブジェクトが取得できず、初期化できないから、 null
を入れておくことは得策ではありません。
そこで、Kotlinでは遅延初期化の機能があります。
lateinit
lateinit
を利用することで「後から使用する前に初期化するので、null非許容で定義します」という宣言をすることができます。
lateinit
は var
に使用することができます。
class User(val name: String, val age: Int) { lateinit var id: Int }
初期化さえしていれば、この id
は必ず null
でないとして呼び出しすることができます。
また、初期化されているかを、 isInitialized
で確認することができます。ただし、 isInitialized
を多用する必要があるのであれば、素直に null許容型にするほうが良いかと思います。
デリゲート(委譲)
デリゲート(委譲)を利用することで、他のインスタンスに処理を任せることができます。
import kotlin.reflect.KProperty class Delegate { operator fun getValue(thisRef: Any?, property: KProperty<*>): String { return "call to get ${property.name}!" } operator fun setValue(thisRef: Any?, property: KProperty<*>, value: String) { println("call to set ${property.name} with ${value}!") } } class User(val name: String, val age: Int) { var delegated by Delegate() }
さらに標準ライブラリには、デリゲートが用意されており、有名なものとしては遅延呼び出しを行える Lazy<T>
があります。これは lazy { ... }
で生成できるため、以下のようにして遅延初期化に使用されます。
class User(val name: String, val age: Int) { val delegated by lazy { println("lazy initialized!") "hello" } }
インスタンス生成だけでは、この lazy
のブロックは呼ばれず、実際にプロパティにアクセスして初めて呼び出されます。
I/Oなどの重い処理を遅延初期化させることで、呼び出すまで遅延させることができます。
また、遅延初期化したプロパティは一度呼び出されると、最初に実行した結果がキャッシュされ、二回目以降はそのキャッシュされたデータが返されるようになります。
まとめ
Kotlinでは多くの初期化処理が存在しているので、初期化順にも気をつけて、上手に使い分けていきたいですね。