Kotlinのlateinitについて

採用はこちら

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 です。

基本ルール

lateinitvar にしか使えない

lateinit は後から代入する前提なので、val には使えません。

lateinit var name: String   // OK
lateinit val name: String   // NG

nullable型には使えない

lateinitnon-null 型専用です。

nullable型にするなら、最初から null を入れておけばよいため、lateinit の役割と重なりません。

lateinit var name: String    // OK
lateinit var name: String?   // NG

primitive type には使えない

IntLongBoolean などの 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

lateinitlazy の違い

この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 を優先することが推奨されています。

lateinitvar 専用なので、必要以上に使わないほうが設計は安定しやすいです。

まとめ

lateinit は、「nullではない必須の値だが、初期化だけ後回しにしたい」ときのための機能です。

押さえるべきポイントは次の通りです。

  • lateinit は non-null の var に使う
  • nullable型と primitive type には使えない
  • トップレベル、クラス本体、ローカル変数で使える
  • クラスプロパティでは primary constructor 不可、custom accessor 不可
  • 初期化前アクセスで UninitializedPropertyAccessException
  • ::prop.isInitialized で確認できる
  • 外から入る値には lateinit、自前で遅延生成する値には lazy
  • まずはコンストラクタ初期化や val を優先し、必要なときだけ lateinit を使う

必要なら次は、lateinitlazy と nullable の違いを表で比較した版にして、さらにわかりやすく整理できます。

以上、Kotlinのlateinitについてでした。

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

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