Kotlin by lazyの仕組みについて

採用はこちら

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 lazylateinit
対象valvar
初期化自動手動
未初期化時問題なし例外
再代入不可可能
null安全

lateinit は「後で必ず代入されることが保証されている」場合向け、lazy は「必要になるまで作りたくない」場合向けです。

メモリリークに関する注意

lazy 自体がリークを起こすわけではありませんが、

  • initializer が短命オブジェクト(Activity など)をキャプチャし
  • lazy プロパティが長生きする

という場合、結果としてリークが発生する可能性があります。

これは lazy 特有の問題ではなく、参照のライフサイクル管理の問題です。

まとめ

by lazy は単なる便利構文ではなく、

  • 初期化タイミングの制御
  • スレッドセーフ設計の選択
  • 例外時の再試行仕様
  • null を排除した安全な設計

をすべて含んだ、Kotlin らしい高水準の抽象です。

以上、Kotlin by lazyの仕組みについてでした。

最後までお読みいただき、ありがとうございました。

よかったらシェアしてね!
  • URLをコピーしました!
  • URLをコピーしました!
目次