この記事で分かること
- Kotlinの lateinit と by lazy の比較と使い分け方について
- lateinit と by lazy の内部実装について
- それぞれのユースケースについての考察
この記事では、Kotlinの言語機能として用意されている lateinit と by lazy の違い について解説しています。
2つともクラスのフィールドを初期化する際に使用される機能ですが、機能が似ているため、それぞれをどのようなケースで適用すれば良いか迷うことがあると思います。
この記事の内容を読むことで、それぞれの特徴や得意なユースケース、概念の違いが分かるようになるため、自信をもって選ぶことが出来るようになります。
次章より、まずは結論を先に述べた上で、lateinit / by lazy のサンプルコードを使って、違いを詳しく解説していきます。
両者の選択に迷った際の指針にしていただければ幸いです。
目次
結論から
まずはそれぞれの設計思想、概念の違いについて結論を述べます。
- lateinit は、インスタンス生成時に確定できない値をあとで初期化するために使用する
- by lazyは、必要になるまで初期化を遅延させるために使用する
2つの利用用途は似ていますが概念の違いを理解して使えば、読み手に対してより明確に意図を伝えることができるようになります。
続いて次章では、lateinit / by lazy それぞれの機能・特徴についてサンプルコードを使ってもう少し詳しく解説します。
lateinit と by lazy の比較
使い分け方の指針となるように、まずはそれぞれの特徴を比較します。
lateinit
- lateinit は必ず var になります(再代入可能)
- lateinit は必ず 非Null許容型(not-nullable) になります
- lateinit のフィールドは必ず初期化されることを期待
- 処理の軽い/重いとは無関係
- 何度でも初期化し直すことが可能
- プリミティブ型は使うことができない
by lazy
- by lazy は、必ず val になります(再代入不可)
- by lazy は Null許容型、非Null許容型 どちらも可能
- by lazy のフィールドは初期化されるとは限らず、使われないこともあり得る
- 主として重い処理を記述するのに使用する
- 初期化処理はプログラム中で1回しか実行できない
- プリミティブ型も使うことができる
lateinit の特徴
本来は、非Null許容型をコンストラクタ以外で初期化したいというニーズのために作られたようです。(https://kotlinlang.org/docs/properties.html#late-initialized-properties-and-variables)
自分で書いたコードを逆コンパイルしてみると分かりますが、lateinit で修飾されたフィールドは内部的には (Nullableの)Javaの参照として実装されており、未初期化状態をnullとして管理しているだけです。そのためプリミティブ型では使えないという制約が生まれたと思われます。
lateinit修飾子は、クラスのインスタンス生成時に値は確定できないが、後ほど初期化して使いたい場合に用います。
サンプルコード
class LateInitSample {
// Stringの場合
lateinit var text: String
// プリミティブ型の場合
lateinit var value: Int // コンパイルエラー
// 独自型の場合
lateinit var myClass: MyClass
}
初期化前にlateinit のフィールドを参照すると、以下のような例外が発生します。
kotlin.UninitializedPropertyAccessException: lateinit property text has not been initialized
初期化前の状態を誤って参照することが無いように設計されているため、比較的安全に利用することが出来ます。
by lazy の特徴
フィールドへの初回アクセス時に初期化処理が実行され、以降フィールドにアクセスされた際は、何度も初期化を実行せずキャッシュされた値を返します。
そのため、コストの掛かる処理を何度も処理する必要がなくなり負荷を下げられます。
アプリの起動時に重い処理をしたくない場合に、値が本当に必要になるまで初期化を遅延させるのが by lazy の役割になります。
内部的には、Lazy<T> クラスを使って実現されています。(https://kotlinlang.org/docs/delegated-properties.html#lazy-properties)
サンプルコード
class ByLazySample {
// Stringの場合
val text: String by lazy {
"initialized by lazy"
}
// プリミティブ型Intの場合
val value: Int by lazyOf(1)
// 独自型の場合
val myClass: MyClass by lazy {
MyClass()
}
// 型は省略可能
val myClass2 by lazy {
MyClass()
}
}
by lazy のロックモード (LazyThreadSafetyMode)
by lazy には、初期化実行時のスレッド制御のオプションがあります。(https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-lazy-thread-safety-mode/)
- LazyThreadSafetyMode.SYNCHRONIZED(同期)
- LazyThreadSafetyMode.PUBLICATION(半同期)
- LazyThreadSafetyMode.NONE(同期なし)
詳しい実装は、LazyJVM.kt ファイルを見ることで確認できます。
SYNCHRONIZEDは、synchronized lock を使って実装されています。
初期化できるスレッドが厳密に1つに制限されるため、初期化処理が1回だけしか行われないことを保証します。
PUBLICATIONは、AtomicReferenceFieldUpdater を使って実装されています。
初期化処理が複数回行われる可能性がありますが、synchronized lockされないため、わずかに効率的である可能性があります。
値はいずれかのスレッドが最初にセットしたものが保持されます。
NONEは、スレッド制御されないため複数スレッドが同時に初期化した場合の挙動は未定義です。
スレッドセーフではないですが、最も速い実装になります。
by lazyのモードを指定しない場合、デフォルトでは LazyThreadSafetyMode.SYNCHRONIZED
が選択されます。(最も遅いが、最も安全)
サンプルコード
class ByLazyLockSample {
val value1: Int by lazy(LazyThreadSafetyMode.SYNCHRONIZED) { 1 } // double checked locking
val value2: Int by lazy(LazyThreadSafetyMode.PUBLICATION) { 2 } // Initialization conflicts may occur. However, the synchronization process is slightly lighter.
val value3: Int by lazy(LazyThreadSafetyMode.NONE) { 3 } // No synchronization
}
使い分け方の指針
- フィールドの初期化が比較的重い処理であり、結果をキャッシュしたい場合は by lazy にします。
- インスタンスの生成時にコンストラクタに値を渡せない場合、lateinit var にします。
- それ以外の場合、通常のフィールドとして定義します。
lateinitは、テストコードのMockや、O/RマッパーのEntityなど、ライブラリのサポートによりフィールドに値を注入するケースでよく使われていると思います。
JavaのライブラリをKotlinで動かす際のアダプタとして使われるケースが多いかもしれません。
まとめ
最後にこの記事の内容をまとめます。
要点
- lateinitは、インスタンス生成時にフィールドの値を確定できない場合に使う
- by lazyは、重い処理を必要になるまで遅延させる場合に使う
- 用途を明確にして使い分けると、読み手に意図が伝わりやすくなる
by lazy は、一度だけ値を取得してキャッシュしたい場合などに使える機能です。
キャッシュ処理を自分で実装することなく、Kotlinの言語機能で利用できることが非常に良いと思います。
あまりデメリットも思いつかないので、使えるケースがあれば積極的に使っても大丈夫な機能だと思います。
lateinit は、インスタンス生成後に別途値を代入する機能ですが、必要のないところで積極的に使う機能では無いと思います。
ライブラリを使うためなどの明確な目的がある場合に、限定的に適用するのが良いと思います。
最後まで読んでいただきありがとうございました。