Отложенная инициализация (lazy initialization) — механизм в Kotlin, при котором свойство объекта инициализируется не в момент создания, а при первом обращении к нему. По данным JetBrains, 2024, lateinit и lazy — два встроенных инструмента для реализации этой стратегии. Оба решают задачу отсрочки инициализации, но принципиально отличаются по механизму работы и области применения.
Главное
Отложенная инициализация — это паттерн, при котором свойство класса получает значение не в момент конструирования объекта, а позже, по требованию. В Kotlin этот паттерн реализован двумя принципиально разными способами: модификатором lateinit и делегатом lazy.
Оба механизма решают общую проблему — свойство должно существовать в классе, но его значение либо ещё неизвестно на момент создания объекта, либо его вычисление слишком ресурсоёмко для выполнения без необходимости. По данным Google I/O 2023, до 40% свойств в типичном Android-приложении можно оптимизировать через отложенную инициализацию, что снижает время запуска на 15–25%.
Выбор между lateinit и lazy определяется тремя факторами: изменяемостью свойства (var или val), временем его жизни (однократное или многократное присвоение) и требованиями к потокобезопасности (однопоточный или многопоточный доступ).
Первый и самый частый сценарий — Dependency Injection. Фреймворк (Dagger, Hilt, Koin) внедряет зависимости после создания объекта, поэтому свойство не может быть проинициализировано в конструкторе. Без lateinit пришлось бы объявлять все зависимости nullable и проверять их при каждом использовании.
Второй сценарий — тяжёлые ресурсы: база данных, сетевой клиент, файловый менеджер. Их создание требует времени и памяти, поэтому они должны инициализироваться только при фактическом использовании. lazy идеально подходит для таких случаев, гарантируя однократное создание.
Третья ситуация — Android-компоненты (Activity, Fragment, ViewModel), жизненный цикл которых управляется операционной системой. Свойства, зависящие от onCreate, onViewCreated или init-блока ViewModel, не могут быть инициализированы в конструкторе.
lateinit — это модификатор для var-свойств, который разрешает компилятору Kotlin отложить инициализацию. Компилятор не требует присвоить значение в конструкторе, но генерирует рантайм-проверку при каждом доступе: если свойство не инициализировано, выбрасывается UninitializedPropertyAccessException.
class MainActivity {
lateinit var binding: ActivityMainBinding
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivityMainBinding.inflate(layoutInflater)
setContentView(binding.root)
}
}
Ограничения lateinit: свойство должно быть объявлено как var (не val), не nullable, не примитивного типа (Int, Double, Boolean и т.д.). Причина — примитивные типы компилируются в JVM-примитивы, у которых нет состояния «не инициализировано». Для nullable-свойств отложенная инициализация не нужна: null уже означает отсутствие значения.
Для проверки состояния lateinit-свойства используется встроенная ссылка через оператор ::: ::propertyName.isInitialized. Это единственный способ безопасно проверить, инициализировано ли свойство, не рискуя получить исключение. Проверка доступна только из того же класса или внутреннего класса, не из внешнего кода.
class LoginFragment {
lateinit var binding: FragmentLoginBinding
fun isReady(): Boolean {
return ::binding.isInitialized
}
}
lateinit не добавляет накладных расходов после инициализации: после присвоения значения доступ к свойству идентичен прямому обращению к полю. Единственная цена — проверка на инициализацию при каждом чтении до присвоения. После инициализации JIT-компилятор оптимизирует проверку.
Важная особенность: lateinit-свойства не могут быть использованы в inline-классах и не поддерживаются для свойств с кастомными getter/setter. Если свойство требует вычисляемого доступа — используйте lazy вместо lateinit.
lazy — это делегат свойства, встроенный в стандартную библиотеку Kotlin. Он вычисляет значение при первом обращении к свойству и кеширует результат для всех последующих вызовов. В отличие от lateinit, lazy работает только с val, делая свойство неизменяемым после инициализации.
class UserRepository {
private val database: Database by lazy {
Database.create("users.db")
}
fun getUser(id: String): User {
return database.query("SELECT * FROM users WHERE id = ?", id)
}
}
lazy принимает необязательный параметр LazyThreadSafetyMode, который управляет механизмом потокобезопасности. По умолчанию используется SYNCHRONIZED — двойная проверка с блокировкой (Double-Checked Locking), гарантирующая однократную инициализацию даже при одновременном доступе из нескольких потоков.
Режим PUBLICATION допускает параллельную инициализацию: несколько потоков могут одновременно выполнить блок инициализации, но результат будет принят только от первого завершившего. Это быстрее SYNCHRONIZED при высокой конкуренции, но увеличивает расход ресурсов.
Режим NONE полностью отключает синхронизацию. Используйте его только для свойств, доступ к которым гарантированно происходит из одного потока. В этом режиме lazy работает с минимальными накладными расходами — практически как прямое присвоение.
val heavyConfig: Config by lazy(LazyThreadSafetyMode.NONE) {
Config.loadFromFile("config.json")
}
lazy — правильный выбор для однократно инициализируемых зависимостей: репозиториев, сетевых клиентов, кэшей, баз данных. Семантика val защищает от случайной перезаписи, а потокобезопасность по умолчанию делает код безопасным в многопоточной среде. lazy также корректно работает с примитивными типами, что невозможно с lateinit.
В Android lazy часто используется для инициализации ViewModel-зависимостей через by viewModels() или для создания ретрофит-клиентов. Однако будьте осторожны: если lazy-блок захватывает ссылку на Activity или Fragment, это может привести к утечке памяти, так как делегат хранит замыкание до конца жизни свойства.
Выбор между lateinit и lazy — это не вопрос предпочтения, а архитектурное решение, определяемое характером свойства. Каждый механизм решает свою задачу, и их области применения пересекаются лишь частично.
| Критерий | lateinit | lazy |
|---|---|---|
| Тип свойства | только var | только val |
| Nullable | запрещён | разрешён |
| Примитивные типы | запрещены | разрешены |
| Потокобезопасность | не гарантируется | SYNCHRONIZED по умолчанию |
| Проверка состояния | ::x.isInitialized | не требуется |
| Исключение при ошибке | UninitializedPropertyAccessException | ошибка в блоке инициализации |
| Кеширование | не применяется | однократное вычисление |
| Android Binding | View Binding, Data Binding | не используется |
| DI фреймворки | Dagger, Hilt, Koin | ручное внедрение |
Используйте lateinit, когда свойство должно изменяться после инициализации или его создание управляется внешним кодом. Типичный пример — View Binding в Android Activity: binding создаётся в onCreate, но остаётся var, поскольку фреймворк не поддерживает val для этого сценария.
Используйте lazy, когда свойство инициализируется однократно, его вычисление дорогое, и значение не меняется в течение жизни объекта. Классический пример — ленивое создание Retrofit-клиента или Room-базы данных при первом обращении к репозиторию.
В одном классе могут одновременно использоваться оба механизма. Например, lateinit для View Binding и lazy для репозитория. Это нормальная практика, отражающая разные требования к разным свойствам. Главное — не путать семантику: не используйте lateinit там, где нужен val, и не используйте lazy для свойств, которые должны перезаписываться.
Самая частая ошибка с lateinit — обращение к свойству до того, как оно проинициализировано. Это приводит к UninitializedPropertyAccessException, который не перехватывается на этапе компиляции, поскольку Kotlin доверяет разработчику корректную последовательность инициализации. Решение — всегда проверять состояние через ::property.isInitialized перед доступом в неоднозначных ситуациях.
Вторая распространённая проблема — использование lateinit для свойств, семантически являющихся val. Если значение устанавливается один раз и больше не изменяется, lazy является более правильным выбором. Он делает свойство неизменяемым, исключает случайную перезапись и добавляет потокобезопасность бесплатно.
Третья ошибка — lazy с побочными эффектами. Блок инициализации lazy не должен изменять внешнее состояние или полагаться на порядок инициализации других lazy-свойств, так как последовательность вычислений зависит от первого обращения и может быть неочевидной. Если lazy-свойства ссылаются друг на друга, это приводит к циклической зависимости и StackOverflowError.
Четвёртая проблема — утечка памяти через lazy в Android. Если lazy-блок захватывает ссылку на Activity или Fragment — делегат хранит замыкание, и сборщик мусора не может освободить компонент даже после его уничтожения. Решение — использовать lazy только с короткоживущими объектами или передавать контекст Application, а не Activity.
Пятая типичная ошибка — попытка применить lateinit к примитивным типам. Компилятор Kotlin блокирует это на уровне синтаксиса, но разработчики пытаются обойти ограничение через nullable-обёртки. Это приводит к лишним проверкам на null и полностью нивелирует преимущества отложенной инициализации.
Часто задаваемые вопросы
lateinit — модификатор для var-свойств, допускающий инициализацию после конструктора. lazy — делегат для val-свойств, вычисляющий значение при первом обращении и кеширующий его. lateinit не поддерживает примитивные типы и nullable, а lazy потокобезопасен по умолчанию.
Да, через встроенную ссылку на свойство: ::propertyName.isInitialized. Метод возвращает true, если свойство уже проинициализировано. Это единственный безопасный способ избежать UninitializedPropertyAccessException при работе с lateinit-полями.
Примитивные типы — Int, Double, Boolean и другие — компилируются в JVM-примитивы (int, double, boolean), у которых нет состояния «не инициализировано». lateinit использует null как флаг, а примитивы не могут быть null, поэтому механизм физически нереализуем для этих типов.
По умолчанию используется LazyThreadSafetyMode.SYNCHRONIZED — двойная проверка с блокировкой, гарантирующая однократную инициализацию при доступе из нескольких потоков. Для однопоточных сценариев используйте NONE, для высокой конкуренции — PUBLICATION.
Когда свойство должно изменяться после инициализации или его создание управляется фреймворком. Типичный пример — View Binding в Android Activity: binding создаётся в onCreate и должен быть var. Для однократно инициализируемых val-зависимостей используйте lazy.
Итоги
Мы разработаем мобильное приложение под ключ
IT Sectr создаёт приложения для iOS и Android для стартапов и бизнеса с 2017 года. Мы проконсультируем вас и предложим наилучшее решение.
Читайте также