lateinit は、「今はまだ初期化できないが、あとで必ず値を代入する非nullの var」を表すための機能です
Kotlinでは通常、非nullのプロパティや変数は宣言時点で初期化が必要ですが、lateinit を使うと、その初期化を後ろにずらせます。
たとえば、次のように使います。
class UserService {
lateinit var repository: UserRepository
}
この repository は宣言時には未初期化ですが、あとから代入できます。
val service = UserService()
service.repository = UserRepository()
ここで大事なのは、repository の型が UserRepository? ではなく、非nullの UserRepository のままだということです。
つまり lateinit は、「nullを許したい」のではなく、「初期化のタイミングだけ遅らせたい」ときに使います。
なぜ lateinit が必要なのか
Kotlinは null 安全を重視するため、通常は非nullの値を未初期化のまま持てません。
ですが実務では、次のような場面があります。
- DIであとから依存オブジェクトが注入される
- テストの
setUp()で初期化する - Androidなどのライフサイクルの途中で生成する
- オブジェクト生成時にはまだ値を用意できない
このような、「使う時点では必ず必要だが、宣言時にはまだ用意できない」ケースを扱うのが lateinit です。
基本ルール
lateinit は var にしか使えない
lateinit は後から代入する前提なので、val には使えません。
lateinit var name: String // OK
lateinit val name: String // NG
nullable型には使えない
lateinit は non-null 型専用です。
nullable型にするなら、最初から null を入れておけばよいため、lateinit の役割と重なりません。
lateinit var name: String // OK
lateinit var name: String? // NG
primitive type には使えない
Int、Long、Boolean などの primitive type には lateinit は使えません。
lateinit var count: Int // NG
lateinit var enabled: Boolean // NG
使える場所
lateinit は次の場所で使えます。
- トップレベルプロパティ
- クラス本体内のプロパティ
- ローカル変数
ここは誤解が多いですが、ローカル変数にも使えます。
fun example() {
lateinit var message: String
message = "Hello"
println(message)
}
クラスプロパティとして使うときの制約
クラスの中で lateinit を使う場合は、いくつか条件があります。
公式ドキュメントでは、lateinit プロパティは クラス本体内で宣言され、custom getter / setter を持たず、primary constructor では宣言できないとされています。
つまり、次のようなものは不可です。
class UserService(lateinit var repo: Repo) // NG
class Sample {
lateinit var name: String
get() = field // NG
}
初期化前に使うとどうなるか
lateinit は便利ですが、初期化前に読み出すと実行時例外になります。
発生するのは UninitializedPropertyAccessException です。
class Sample {
lateinit var text: String
fun printText() {
println(text)
}
}
fun main() {
val s = Sample()
s.printText() // 例外
}
つまり lateinit は、コンパイル時に完全に安全を保証する機能ではなく、「使う前には必ず初期化する」という責任を開発者側が持つ仕組みです。
初期化済みか確認する方法
lateinit には、初期化済みかどうかを確認するための isInitialized があります。
::property.isInitialized は、その lateinit プロパティに値が代入済みなら true、未代入なら false を返します。
class Sample {
lateinit var name: String
fun printName() {
if (::name.isInitialized) {
println(name)
} else {
println("まだ初期化されていません")
}
}
}
ただし、isInitialized には範囲の制約があります。
そのプロパティにアクセスできる場所でしか使えず、公式ドキュメントでは 同じクラス、外側のクラス、または同じファイルのトップレベルプロパティで使えると説明されています。
また、APIでは inline function では使えないとも明記されています。
lateinit と nullable の違い
これはとても重要です。
nullable
var name: String? = null
これは、「値がないこと自体が自然な状態」を表します。
そのため、使うたびに null チェックが必要です。
println(name?.length)
lateinit
lateinit var name: String
こちらは、「今は未初期化だが、使う時点では必ず値が入っている」という設計です。
使う側は non-null として扱えますが、初期化前アクセスは例外になります。
使い分けの目安
- 値がないことが普通にあり得る → nullable
- 値は必須だが、初期化だけ遅い →
lateinit
lateinit と lazy の違い
この2つもよく混同されます。
lateinit
- 後から手動で代入する
var- 非null参照型向け
- 初期化前アクセスで例外
lazy
- 最初にアクセスしたときに自動初期化する
- 通常は
valと組み合わせる - 一度初期化された値は、その
Lazyの生存期間中は変わらない - 初期化時に例外が起きた場合、次回アクセス時に再試行される
val repository: UserRepository by lazy {
UserRepository()
}
使い分け
- 外からあとで入る値 →
lateinit - 自分で必要時に生成する値 →
lazy
この整理がいちばん実務的です。
実務でよくある使いどころ
テストコード
lateinit は、テストの setUp() で毎回初期化するケースと相性がいいです。
公式ドキュメントでも代表例として紹介されています。
class UserServiceTest {
private lateinit var service: UserService
@Before
fun setUp() {
service = UserService()
}
}
DIやライフサイクル依存の初期化
生成直後には値がなく、あとから確実にセットされる設計にも向いています。
class Controller {
lateinit var service: Service
fun execute() {
println(service)
}
}
ただし、この場合は execute() の前に必ず service が代入されることを保証しないと危険です。
よくある落とし穴
宣言しただけで使えると思ってしまう
class Sample {
lateinit var text: String
}
fun main() {
val s = Sample()
println(s.text) // 例外
}
lateinit は「自動初期化」ではなく、「あとで自分で入れる宣言」です。
初期化順序があいまいになる
lateinit を増やしすぎると、「どこで初期化されるのか」「呼び出し順が正しいか」が見えにくくなります。
結果として、ランタイム例外が起きやすくなります。これは lateinit の代表的な弱点です。
lateinit を使わないほうがいいケース
lateinit は便利ですが、第一選択ではないことも多いです。
コンストラクタで受け取れるなら、そちらが安全
class UserService(
private val repository: UserRepository
)
この形なら、生成時点で必要な依存がそろうため、未初期化の問題が起きません。
自分で遅延生成できるなら lazy
val repo by lazy { Repo() }
値がないこと自体が自然なら nullable
var selectedUser: User? = null
また、Kotlinのコーディング規約では、変更しないなら var より val を優先することが推奨されています。
lateinit は var 専用なので、必要以上に使わないほうが設計は安定しやすいです。
まとめ
lateinit は、「nullではない必須の値だが、初期化だけ後回しにしたい」ときのための機能です。
押さえるべきポイントは次の通りです。
lateinitは non-null のvarに使う- nullable型と primitive type には使えない
- トップレベル、クラス本体、ローカル変数で使える
- クラスプロパティでは primary constructor 不可、custom accessor 不可
- 初期化前アクセスで
UninitializedPropertyAccessException ::prop.isInitializedで確認できる- 外から入る値には
lateinit、自前で遅延生成する値にはlazy - まずはコンストラクタ初期化や
valを優先し、必要なときだけlateinitを使う
必要なら次は、lateinit と lazy と nullable の違いを表で比較した版にして、さらにわかりやすく整理できます。
以上、Kotlinのlateinitについてでした。
最後までお読みいただき、ありがとうございました。










