Что такое lateinit var и by lazy?

В Kotlin lateinit var и by lazy — это два разных способа отложенной инициализации переменных, каждый со своей областью применения и особенностями.

lateinit var

lateinit используется только с изменяемыми переменными (var) и только с не-null типами. Он говорит компилятору: «я обещаю, что инициализирую эту переменную до её первого использования». Используется в ситуациях, когда нельзя сразу присвоить значение в момент объявления переменной, но гарантируется, что оно появится позже (например, в конструкторе или setup() методе).

Пример:

class MyActivity : AppCompatActivity() {
private lateinit var viewModel: MyViewModel
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
viewModel = ViewModelProvider(this).get(MyViewModel::class.java)
}
fun doSomething() {
viewModel.loadData()
}
}

Особенности:

  • Только для var и не может быть применён к типам, допускающим null.

  • Используется только с типами-ссылками (нельзя применить к Int, Boolean, Double и т.п.).

  • Если попытаться использовать lateinit переменную до инициализации, произойдёт исключение UninitializedPropertyAccessException.

  • Можно проверить, инициализирована ли переменная, с помощью ::propertyName.isInitialized (только для lateinit в классе, не в top-level):

if (::viewModel.isInitialized) {
viewModel.loadData()
}

by lazy

by lazy — это делегат, применяемый для val-переменных (неизменяемых), которые инициализируются только при первом доступе. После первой инициализации значение кэшируется, инициализация происходит один раз. Это удобно для тяжёлых объектов, которые нужны не всегда, или которые не стоит создавать заранее.

Пример:

val database: Database by lazy {
println("Инициализация базы данных...")
Database.connect()
}

Переменная database будет инициализирована один раз — только тогда, когда к ней впервые обратятся, а не при запуске программы.

Особенности:

  • Работает только с val, поскольку значение должно быть неизменным после первого присваивания.

  • Используется для ленивой инициализации тяжёлых или опциональных объектов.

  • По умолчанию, by lazy потокобезопасен (LazyThreadSafetyMode.SYNCHRONIZED). Это может быть изменено:

val someValue: String by lazy(LazyThreadSafetyMode.NONE) {
computeValue()
}

Режимы LazyThreadSafetyMode:

  • SYNCHRONIZED — гарантирует, что инициализация будет выполнена только одним потоком (по умолчанию).

  • PUBLICATION — инициализация может быть выполнена несколькими потоками, но значение будет опубликовано один раз.

  • NONE — не потокобезопасен, но быстрее (для однопоточной инициализации, например, в UI).

Краткое сравнение:

lateinit var by lazy
Ключевое слово lateinit by lazy
--- --- ---
Тип переменной var val
--- --- ---
Nullability Только non-null Любые
--- --- ---
Применим к примитивам ✅ (например, val x: Int by lazy)
--- --- ---
Проверка инициализации ::prop.isInitialized (внутри класса) Нет встроенной проверки
--- --- ---
Используется для DI, Android view binding, Unit tests Тяжёлые или опциональные объекты
--- --- ---
Исключение при доступе до инициализации UninitializedPropertyAccessException Не возникает — просто не инициализировано
--- --- ---

Оба инструмента полезны в разных контекстах. lateinit даёт гибкость при инициализации позже, когда использование конструктора невозможно или неудобно. by lazy — идеальный способ инкапсулировать отложенное вычисление, при этом сохраняя неизменяемость и потокобезопасность.