はじめに
Kotlinでは「値をすぐに計算せず、必要になった時にだけ計算する」ことができます。
それを実現するのが lazy。
つまり、
「最初にアクセスされた瞬間に初期化される
val」
です。
メモリ効率や初期化順序を最適化できるため、
Android・サーバー・テストなど、あらゆる場面で活用されます。
基本構文
val value by lazy { initializer }
-
by lazy { ... }の中に初期化処理を書く - 初めてアクセスされた時だけ、その処理が実行される
- 2回目以降はキャッシュされた値を返す
例
val userName by lazy {
println("Initializing userName...")
"Anna"
}
fun main() {
println("Before access")
println(userName)
println(userName)
}
出力:
Before access
Initializing userName...
Anna
Anna
⚙️ 初期化は1回だけ。2回目以降はキャッシュ済み。
lateinit との違い
| 比較項目 | lateinit |
lazy |
|---|---|---|
| 修飾対象 | var |
val |
| 初期化タイミング | 手動で代入した時 | 最初にアクセスされた時 |
| null許容型 | ❌ 不可 | ✅ 可 |
| スレッドセーフ | ❌ デフォルト非対応 | ✅ デフォルトで対応 |
| 例外発生タイミング | 未初期化アクセス時(実行時) | 初期化処理で例外が起きた時のみ |
| 主な用途 | DI・View・テスト | キャッシュ・重い初期化処理 |
lazy の動作モード
Kotlinの lazy() 関数には3種類の動作モードがあります。
SYNCHRONIZED(デフォルト)
複数スレッドから同時アクセスされても一度だけ初期化。
val config by lazy(LazyThreadSafetyMode.SYNCHRONIZED) {
loadConfig()
}
PUBLICATION
複数スレッドが同時に初期化しても、一番最後の値だけ保持。
val data by lazy(LazyThreadSafetyMode.PUBLICATION) {
fetchData()
}
NONE
スレッドセーフではないが最も高速。
val cache by lazy(LazyThreadSafetyMode.NONE) {
createCache()
}
Androidなどシングルスレッド環境では
NONEが高速で有効。
lazy の内部構造
lazy の戻り値は Lazy<T> インターフェース。
public interface Lazy<out T> {
val value: T
fun isInitialized(): Boolean
}
by lazy は実は Lazy<T> の委譲プロパティ(Property Delegation)です。
val foo by lazy { 42 }
// 実際には以下のように展開される
private val _foo = lazy { 42 }
val foo get() = _foo.value
Kotlinの
byは「プロパティ委譲構文」。
lazyはその代表的な実装のひとつ。
lazy の状態を確認する
val result by lazy { compute() }
println(result.isInitialized()) // ❌ エラー(直接呼べない)
実は lazy 自体を参照して確認する必要があります。
val resultLazy = lazy { compute() }
val result by resultLazy
println(resultLazy.isInitialized()) // false
println(result) // compute() 実行
println(resultLazy.isInitialized()) // true
実践例1:重い初期化を遅らせる
class DataRepository {
val database by lazy { Database.connect() }
}
データベース接続はコストが高いため、必要になるまで初期化しない設計。
実践例2:Android Activity
class MainActivity : AppCompatActivity() {
private val binding by lazy { ActivityMainBinding.inflate(layoutInflater) }
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(binding.root)
}
}
lateinitではなくlazyを使うと、安全に遅延初期化+valで不変にできる。
実践例3:キャッシュ利用
val config by lazy {
println("Loading config...")
loadConfigFile()
}
fun loadConfigFile(): Map<String, String> {
// 重いファイルIOをシミュレート
Thread.sleep(1000)
return mapOf("theme" to "dark", "lang" to "ja")
}
fun main() {
println(config["theme"]) // 初回:読み込み発生
println(config["lang"]) // 2回目:キャッシュ利用
}
注意点とアンチパターン
| パターン | 問題点 |
|---|---|
lazy で重すぎる処理をUIスレッドで実行 |
初回アクセス時にラグが発生 |
lazy 内で例外が発生 |
再アクセスしても再初期化されない |
lateinit と混同して使用 |
目的が異なる(val vs var) |
| キャッシュとして長期保持 | メモリリークの原因になる場合あり |
まとめ
| ポイント | 説明 |
|---|---|
lazy は「最初にアクセスされた時だけ初期化」 |
|
デフォルトはスレッドセーフ(SYNCHRONIZED) |
|
by lazy は委譲プロパティ構文を利用 |
|
| 重い処理・設定・View初期化などに最適 | |
Androidでは lateinit より lazy + val が安全なケース多数 |