Відкладена ініціалізація (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 — подвійна перевірка з блокуванням, що гарантує одноразову ініціалізацію навіть при одночасному доступі з кількох потоків.
Режим 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 року. Ми проконсультуємо вас і запропонуємо найкраще рішення.
Читайте також