Kotlin の by lazy は、単なる「遅延評価」の糖衣構文ではありません。
委譲プロパティ・スレッドセーフ設計・例外時の再試行仕様まで含めて設計された、非常に完成度の高い仕組みです。
この記事では、
by lazyが 何を保証し、何を保証しないのか- 内部的に どのように動作しているのか
- 実務で 安全に使うための正確な理解
を中心に解説します。
by lazy の基本動作
by lazy を使ったプロパティは、
- 最初にアクセスされたときに初期化され
- 初期化結果を内部に保持し
- 以降は同じ値を返し続ける
という振る舞いをします。
val message: String by lazy {
println("initialize")
"Hello"
}
println(message)
println(message)
出力
initialize
Hello
Hello
重要なのは、
- 初期化コードは 1回だけ実行される
- プロパティ宣言時には 何も実行されない
という点です。
なぜ lazy が必要なのか
通常の val は、オブジェクト生成時に必ず初期化されます。
val config = loadHugeConfig() // 常に実行される
この設計には次の問題があります。
- 実際には使わない場合でも処理が走る
- 起動時間が延びる
- 不要な I/O や計算が発生する
by lazy はこれを解決し、
- 必要になったときだけ
- 1回だけ
- 安全に
初期化するための仕組みです。
by lazy は委譲プロパティである
by lazy は 委譲プロパティ(Delegated Property) の一種です。
val x by lazy { ... }
このコードは、コンパイラによって概念的には次のように展開されます。
private val x$delegate = lazy { ... }
val x: T
get() = x$delegate.value
つまり、
- 値そのものを持つのではなく
- 値を管理するオブジェクト(delegate)に処理を委ねている
という構造です。
lazy 関数と Lazy<T> インターフェース
lazy は次のシグネチャを持つ関数です。
fun <T> lazy(initializer: () -> T): Lazy<T>
戻り値は Lazy<T> インターフェースです。
interface Lazy<out T> {
val value: T
fun isInitialized(): Boolean
}
value:実際の値(必要なら初期化される)isInitialized():すでに初期化済みかどうか
内部動作のイメージ(概念)
lazy の挙動を単純化すると、次のような仕組みになります。
class SimpleLazy<T>(val initializer: () -> T) {
private var value: Any? = UNINITIALIZED
fun get(): T {
if (value === UNINITIALIZED) {
value = initializer()
}
return value as T
}
}
重要なポイントは
- 未初期化状態を保持している
- 初回アクセス時にだけ initializer を実行
- 結果を保存して再利用
という点です。
スレッドセーフ設計
by lazy には 3つのスレッドセーフモードがあります。
SYNCHRONIZED(デフォルト)
val value by lazy {
heavyTask()
}
特徴
- 完全にスレッドセーフ
- 複数スレッドから同時アクセスされても
initializer は1回しか実行されない - 内部で
synchronizedを使用
注意点
- ロック対象は Lazy インスタンス自身
- 外部で同じオブジェクトを同期に使うと、デッドロックの危険がある
PUBLICATION
val value by lazy(LazyThreadSafetyMode.PUBLICATION) {
heavyTask()
}
正確な挙動
- initializer が 複数回呼ばれる可能性がある
- ただし Lazy の値として採用されるのは1つだけ
- どの値が採用されるかは保証されない
向いているケース
- 初期化処理が軽い
- 副作用がない
- パフォーマンスを優先したい場合
※「最後の値が勝つ」とは限らない点に注意が必要です。
NONE
val value by lazy(LazyThreadSafetyMode.NONE) {
heavyTask()
}
特徴
- 一切スレッドセーフではない
- ロックなし(最速)
- マルチスレッド環境では挙動未保証
向いているケース
- UI スレッド限定
- シングルスレッド前提の処理
- Android の Activity / ViewModel 内など
初期化時に例外が発生した場合
これは 見落とされやすい重要仕様です。
- initializer が 例外を投げた場合
- lazy プロパティは 未初期化のまま
- 次のアクセス時に 再度 initializer が実行される
つまり、
val value by lazy {
error("fail")
}
この場合、アクセスするたびに再試行されます。
副作用のある initializer を書く場合は特に注意が必要です。
lateinit との違い
| 項目 | by lazy | lateinit |
|---|---|---|
| 対象 | val | var |
| 初期化 | 自動 | 手動 |
| 未初期化時 | 問題なし | 例外 |
| 再代入 | 不可 | 可能 |
| null安全 | ◎ | △ |
lateinit は「後で必ず代入されることが保証されている」場合向け、lazy は「必要になるまで作りたくない」場合向けです。
メモリリークに関する注意
lazy 自体がリークを起こすわけではありませんが、
- initializer が短命オブジェクト(Activity など)をキャプチャし
- lazy プロパティが長生きする
という場合、結果としてリークが発生する可能性があります。
これは lazy 特有の問題ではなく、参照のライフサイクル管理の問題です。
まとめ
by lazy は単なる便利構文ではなく、
- 初期化タイミングの制御
- スレッドセーフ設計の選択
- 例外時の再試行仕様
- null を排除した安全な設計
をすべて含んだ、Kotlin らしい高水準の抽象です。
以上、Kotlin by lazyの仕組みについてでした。
最後までお読みいただき、ありがとうございました。










