遅延初期化(lazy initialization)は、Kotlinにおいてオブジェクトのプロパティが作成時ではなく、最初にアクセスされた時に初期化されるメカニズムです。JetBrains, 2024によると、lateinitとlazyはこの戦略を実装するための2つの組み込みツールです。どちらも遅延初期化の問題を解決しますが、動作メカニズムと適用範囲が根本的に異なります。
重要なポイント
遅延初期化は、クラスのプロパティがオブジェクト構築時ではなく、後で必要に応じて値を受け取るパターンです。Kotlinでは、このパターンは2つの根本的に異なる方法で実装されています: lateinit修飾子とlazyデリゲートです。
どちらのメカニズムも共通の問題を解決します — プロパティはクラスに存在しなければなりませんが、その値はオブジェクト作成時に不明であるか、不必要に実行するにはリソース集約的すぎるかのいずれかです。Google I/O 2023によると、典型的なAndroidアプリケーションの最大40%のプロパティが遅延初期化によって最適化でき、起動時間を15~25%削減できます。
lateinitとlazyの選択は、3つの要因によって決まります: プロパティの可変性(varかvalか)、そのライフサイクル(単一または複数回の割り当て)、およびスレッドセーフティの要件(シングルスレッドかマルチスレッドアクセスか)です。
最初で最も一般的なシナリオは依存性注入です。フレームワーク(Dagger、Hilt、Koin)はオブジェクト作成後に依存関係を注入するため、プロパティをコンストラクタで初期化できません。lateinitがない場合、すべての依存関係をnullableとして宣言し、使用するたびにチェックする必要があります。
2番目のシナリオは重いリソースです: データベース、ネットワーククライアント、ファイルマネージャーなど。これらの作成には時間とメモリが必要なため、実際に使用される時だけ初期化する必要があります。lazyはこのような場合に最適で、一度だけの作成を保証します。
3番目の状況はAndroidコンポーネント(Activity、Fragment、ViewModel)で、そのライフサイクルはオペレーティングシステムによって管理されます。onCreate、onViewCreated、またはViewModelのinitブロックに依存するプロパティは、コンストラクタで初期化できません。
lateinitはvarプロパティ用の修飾子で、Kotlinコンパイラが初期化を延期できるようにします。コンパイラはコンストラクタでの値の代入を要求しませんが、アクセスごとに実行時チェックを生成します: プロパティが初期化されていない場合、UninitializedPropertyAccessExceptionをスローします。
class MainActivity {
lateinit var binding: ActivityMainBinding
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivityMainBinding.inflate(layoutInflater)
setContentView(binding.root)
}
}
lateinitの制限: プロパティはvar(valではない)、非null、かつプリミティブ型(Int、Double、Booleanなど)以外で宣言する必要があります。理由は、プリミティブ型はJVMプリミティブにコンパイルされ、“未初期化”状態がないためです。null許容プロパティの場合、遅延初期化は不要です: nullはすでに値がないことを意味します。
lateinitプロパティの状態を確認するには、::演算子を使用した組み込み参照を使用します: ::propertyName.isInitialized。これは、例外のリスクなしにプロパティが初期化されているかどうかを確認する唯一の安全な方法です。このチェックは同じクラスまたは内部クラスからのみ利用可能で、外部コードからは利用できません。
class LoginFragment {
lateinit var binding: FragmentLoginBinding
fun isReady(): Boolean {
return ::binding.isInitialized
}
}
lateinitは初期化後にオーバーヘッドを追加しません: 値が割り当てられた後、プロパティアクセスは直接フィールドアクセスと同じです。唯一のコストは、割り当て前の読み取りごとの初期化チェックです。初期化後、JITコンパイラはチェックを最適化します。
重要な注意点: lateinitプロパティはインラインクラスで使用できず、カスタムgetter/setterを持つプロパティではサポートされません。プロパティが計算アクセスを必要とする場合は、lateinitの代わりにlazyを使用してください。
lazyはKotlin標準ライブラリに組み込まれたプロパティデリゲートです。プロパティへの最初のアクセス時に値を計算し、後続のすべての呼び出しのために結果をキャッシュします。lateinitとは異なり、lazyはvalでのみ動作し、初期化後にプロパティを不変にします。
class UserRepository {
private val database: Database by lazy {
Database.create("users.db")
}
fun getUser(id: String): User {
return database.query("SELECT * FROM users WHERE id = ?", id)
}
}
lazyはスレッドセーフメカニズムを制御するオプションのパラメータLazyThreadSafetyModeを受け入れます。デフォルトはSYNCHRONIZEDで、ロック付きのダブルチェック方式で、複数のスレッドからの同時アクセス下でも単一の初期化を保証します。
PUBLICATIONモードは並行初期化を許可します: 複数のスレッドが同時に初期化ブロックを実行できますが、結果は最初に完了したスレッドからのみ受け入れられます。これは高コンテンション下ではSYNCHRONIZEDより高速ですが、リソース消費が増加します。
NONEモードは同期を完全に無効にします。単一のスレッドからのアクセスが保証されているプロパティにのみ使用してください。このモードでは、lazyは最小限のオーバーヘッドで動作します — ほぼ直接代入と同様です。
val heavyConfig: Config by lazy(LazyThreadSafetyMode.NONE) {
Config.loadFromFile("config.json")
}
lazyは一度だけ初期化される依存関係に適切な選択肢です: リポジトリ、ネットワーククライアント、キャッシュ、データベース。valのセマンティクスは誤った上書きから保護し、デフォルトのスレッドセーフティはマルチスレッド環境でコードを安全にします。lazyはプリミティブ型でも正しく動作し、これはlateinitでは不可能です。
Androidでは、lazyはby viewModels()によるViewModelの依存関係の初期化やRetrofitクライアントの作成によく使用されます。ただし、注意が必要です: lazyブロックがActivityやFragmentへの参照をキャプチャすると、メモリリークを引き起こす可能性があります。デリゲートはプロパティの存続期間中クロージャを保持するためです。
lateinitとlazyの選択は好みの問題ではなく、プロパティの性質によって決まるアーキテクチャ上の決定です。各メカニズムはそれぞれのタスクを解決し、それらの適用領域は部分的にのみ重なります。
| 基準 | lateinit | lazy |
|---|---|---|
| プロパティ型 | varのみ | valのみ |
| Null許容 | 不可 | 可 |
| プリミティブ型 | 不可 | 可 |
| スレッドセーフティ | 保証なし | デフォルトでSYNCHRONIZED |
| 状態確認 | ::x.isInitialized | 不要 |
| エラー時の例外 | UninitializedPropertyAccessException | initブロックのエラー |
| キャッシュ | 該当なし | 単一計算 |
| Android Binding | View Binding, Data Binding | 使用しない |
| DIフレームワーク | Dagger, Hilt, Koin | 手動注入 |
lateinitは、プロパティが初期化後に変更される必要がある場合、またはその作成が外部コードによって管理される場合に使用します。典型的な例はAndroid ActivityのView Bindingです: bindingはonCreateで作成されますが、フレームワークがこのシナリオでvalをサポートしないため、varのままです。
lazyは、プロパティが一度だけ初期化され、その計算が高コストで、オブジェクトの存続期間中に値が変わらない場合に使用します。典型的な例は、リポジトリへの最初のアクセス時のRetrofitクライアントまたはRoomデータベースの遅延作成です。
両方のメカニズムを同じクラス内で同時に使用できます。例えば、View Bindingにはlateinit、リポジトリにはlazyという具合です。これは異なるプロパティに対する異なる要件を反映した正常な実践です。重要なのはセマンティクスを混同しないことです: valが必要な場面でlateinitを使用せず、再割り当てが必要なプロパティにlazyを使用しないでください。
lateinitで最もよくある間違いは、プロパティが初期化される前にアクセスすることです。これはUninitializedPropertyAccessExceptionを引き起こし、Kotlinが開発者に正しい初期化順序を信頼するため、コンパイル時には捕捉されません。解決策は、曖昧な状況ではアクセス前に::property.isInitializedで常に状態を確認することです。
2番目によくある問題は、意味的にvalであるプロパティにlateinitを使用することです。値が一度設定されて二度と変更されない場合、lazyがより正しい選択です。プロパティを不変にし、誤った上書きを防ぎ、スレッドセーフティを無料で追加します。
3番目の間違いは副作用のあるlazyです。lazy初期化ブロックは外部状態を変更したり、他のlazyプロパティの初期化順序に依存したりするべきではありません。計算順序は最初のアクセスに依存し、明白でない可能性があるためです。lazyプロパティが互いに参照し合うと、循環依存とStackOverflowErrorを引き起こします。
4番目の問題はAndroidでのlazyによるメモリリークです。lazyブロックがActivityやFragmentへの参照をキャプチャすると、デリゲートがクロージャを保持し、ガベージコレクタはコンポーネントが破棄された後でも解放できません。解決策は、短命なオブジェクトでのみlazyを使用するか、Activityの代わりにApplicationコンテキストを渡すことです。
5番目の典型的な間違いは、プリミティブ型にlateinitを適用しようとすることです。Kotlinコンパイラは構文レベルでこれをブロックしますが、開発者はnull許容ラッパーを介して制限を回避しようとします。これにより不要なnullチェックが発生し、遅延初期化の利点が完全に無効になります。
よくある質問
lateinitはvarプロパティ用の修飾子で、コンストラクタ後の初期化を許可します。lazyはvalプロパティ用のデリゲートで、最初のアクセス時に値を計算してキャッシュします。lateinitはプリミティブ型とnull許容をサポートしませんが、lazyはデフォルトでスレッドセーフです。
はい、組み込みのプロパティ参照を使用します: ::propertyName.isInitialized。プロパティが初期化されている場合、メソッドはtrueを返します。これはlateinitフィールドを扱う際にUninitializedPropertyAccessExceptionを回避する唯一の安全な方法です。
プリミティブ型 — Int、Double、Booleanなど — はJVMプリミティブ(int、double、boolean)にコンパイルされ、“未初期化”状態がありません。lateinitはnullをフラグとして使用しますが、プリミティブはnullになれないため、これらの型ではメカニズムが物理的に不可能です。
デフォルトはLazyThreadSafetyMode.SYNCHRONIZEDです — ロック付きのダブルチェック方式で、複数のスレッドからの同時アクセス下で単一の初期化を保証します。シングルスレッドのシナリオではNONEを、高コンテンションではPUBLICATIONを使用してください。
プロパティが初期化後に変更される必要がある場合、またはその作成がフレームワークによって管理される場合です。典型的な例はAndroid ActivityのView Bindingです: bindingはonCreateで作成され、varである必要があります。一度だけ初期化されるval依存関係にはlazyを使用してください。
まとめ
ターンキー方式のモバイルアプリケーションを開発します
IT Sectrは2017年からスタートアップや企業向けにiOS・Androidアプリケーションを開発しています。私たちがご相談に乗り、最適なソリューションをご提案します。