lateinit / lazy: суть відкладеної ініціалізації та її механізми в Kotlin

Автор: IT Sectr Опубліковано: 2026-06-23 Час читання: 8 хв

Відкладена ініціалізація (lazy initialization) — механізм у Kotlin, за якого властивість об’єкта ініціалізується не в момент створення, а при першому зверненні до неї. За даними JetBrains, 2024, lateinit і lazy — два вбудовані інструменти для реалізації цієї стратегії. Обидва вирішують завдання відстрочки ініціалізації, але принципово відрізняються за механізмом роботи та областю застосування.

Головне

  • lateinit — модифікатор для var-властивостей, що допускає ініціалізацію після створення об’єкта
  • lazy — делегат для val-властивостей, що ініціалізує значення при першому зверненні
  • lateinit вимагає var і не підтримує примітивні типи JVM
  • lazy потокобезпечний за замовчуванням і кешує обчислений результат
  • lateinit викидає UninitializedPropertyAccessException при передчасному доступі

Що таке відкладена ініціалізація в Kotlin?

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

lateinit — це модифікатор для var-властивостей, який дозволяє компілятору Kotlin відкласти ініціалізацію. Компілятор не вимагає присвоєння значення в конструкторі, але генерує рантайм-перевірку при кожному доступі: якщо властивість не ініціалізована, викидається UninitializedPropertyAccessException.

kotlin
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. Це єдиний безпечний спосіб перевірити, чи ініціалізовано властивість, не ризикуючи отримати виняток. Перевірка доступна лише з того самого класу або внутрішнього класу, не із зовнішнього коду.

kotlin
class LoginFragment {
    lateinit var binding: FragmentLoginBinding

    fun isReady(): Boolean {
        return ::binding.isInitialized
    }
}

Продуктивність lateinit

lateinit не додає накладних витрат після ініціалізації: після присвоєння значення доступ до властивості ідентичний прямому зверненню до поля. Єдина ціна — перевірка на ініціалізацію при кожному читанні до присвоєння. Після ініціалізації JIT-компілятор оптимізує перевірку.

Важлива особливість: lateinit-властивості не можуть бути використані в inline-класах і не підтримуються для властивостей з кастомними getter/setter. Якщо властивість потребує обчислюваного доступу — використовуйте lazy замість lateinit.

lazy: механізм і переваги

lazy — це делегат властивості, вбудований у стандартну бібліотеку Kotlin. Він обчислює значення при першому зверненні до властивості та кешує результат для всіх наступних викликів. На відміну від lateinit, lazy працює тільки з val, роблячи властивість незмінною після ініціалізації.

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

Режими потокобезпечності lazy

Режим PUBLICATION допускає паралельну ініціалізацію: кілька потоків можуть одночасно виконати блок ініціалізації, але результат буде прийнято лише від першого завершеного. Це швидше SYNCHRONIZED при високій конкуренції, але збільшує витрати ресурсів.

Режим NONE повністю вимикає синхронізацію. Використовуйте його лише для властивостей, доступ до яких гарантовано відбувається з одного потоку. У цьому режимі lazy працює з мінімальними накладними витратами — практично як пряме присвоєння.

kotlin
val heavyConfig: Config by lazy(LazyThreadSafetyMode.NONE) {
    Config.loadFromFile("config.json")
}

Коли lazy переважніше

lazy — правильний вибір для одноразово ініціалізованих залежностей: репозиторіїв, мережевих клієнтів, кешів, баз даних. Семантика val захищає від випадкового перезапису, а потокобезпечність за замовчуванням робить код безпечним у багатопотоковому середовищі. lazy також коректно працює з примітивними типами, що неможливо з lateinit.

В Android lazy часто використовується для ініціалізації ViewModel-залежностей через by viewModels() або для створення ретрофіт-клієнтів. Однак будьте обережні: якщо lazy-блок захоплює посилання на Activity або Fragment, це може призвести до витоку пам’яті, оскільки делегат зберігає замикання до кінця життя властивості.

lateinit vs lazy: порівняння підходів

Вибір між lateinit і lazy — це не питання вподобання, а архітектурне рішення, що визначається характером властивості. Кожен механізм вирішує своє завдання, і їх області застосування перетинаються лише частково.

Критерійlateinitlazy
Тип властивостітільки varтільки val
Nullableзабороненодозволено
Примітивні типизабороненодозволено
Потокобезпечністьне гарантуєтьсяSYNCHRONIZED за замовчуванням
Перевірка стану::x.isInitializedне потрібна
Виняток при помилціUninitializedPropertyAccessExceptionпомилка в блоці ініціалізації
Кешуванняне застосовуєтьсяодноразове обчислення
Android BindingView Binding, Data Bindingне використовується
DI фреймворкиDagger, Hilt, Koinручне впровадження

Використовуйте lateinit, коли властивість має змінюватися після ініціалізації або її створення керується зовнішнім кодом. Типовий приклад — View Binding в Android Activity: binding створюється в onCreate, але залишається var, оскільки фреймворк не підтримує val для цього сценарію.

Використовуйте lazy, коли властивість ініціалізується одноразово, її обчислення дороге, і значення не змінюється протягом життя об’єкта. Класичний приклад — ліниве створення Retrofit-клієнта або Room-бази даних при першому зверненні до репозиторію.

Комбінування lateinit і lazy

В одному класі можуть одночасно використовуватися обидва механізми. Наприклад, lateinit для View Binding і lazy для репозиторію. Це нормальна практика, що відображає різні вимоги до різних властивостей. Головне — не плутати семантику: не використовуйте lateinit там, де потрібен val, і не використовуйте lazy для властивостей, які мають перезаписуватися.

Типові помилки при використанні lateinit і 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 і lazy в Kotlin?

lateinit — модифікатор для var-властивостей, що допускає ініціалізацію після конструктора. lazy — делегат для val-властивостей, що обчислює значення при першому зверненні та кешує його. lateinit не підтримує примітивні типи і nullable, а lazy потокобезпечний за замовчуванням.

Чи можна перевірити, чи ініціалізовано lateinit-властивість?

Так, через вбудоване посилання на властивість: ::propertyName.isInitialized. Метод повертає true, якщо властивість уже проініціалізовано. Це єдиний безпечний спосіб уникнути UninitializedPropertyAccessException при роботі з lateinit-полями.

Чому lateinit не можна використовувати з примітивними типами?

Примітивні типи — Int, Double, Boolean та інші — компілюються в JVM-примітиви (int, double, boolean), у яких немає стану “не ініціалізовано”. lateinit використовує null як прапорець, а примітиви не можуть бути null, тому механізм фізично нереалізований для цих типів.

Який режим потокобезпечності у lazy за замовчуванням?

За замовчуванням використовується LazyThreadSafetyMode.SYNCHRONIZED — подвійна перевірка з блокуванням, що гарантує одноразову ініціалізацію при доступі з кількох потоків. Для однопотокових сценаріїв використовуйте NONE, для високої конкуренції — PUBLICATION.

Коли варто використовувати lateinit замість lazy в Android?

Коли властивість має змінюватися після ініціалізації або її створення керується фреймворком. Типовий приклад — View Binding в Android Activity: binding створюється в onCreate і має бути var. Для одноразово ініціалізованих val-залежностей використовуйте lazy.

Підсумки

  • lateinit — модифікатор для var-властивостей Kotlin, що дозволяє ініціалізацію після конструктора без nullable
  • lazy — делегат властивості для val з одноразовим обчисленням і автоматичним кешуванням результату
  • lateinit викидає UninitializedPropertyAccessException при доступі до ініціалізації
  • lazy потокобезпечний за замовчуванням через LazyThreadSafetyMode.SYNCHRONIZED
  • lateinit несумісний з примітивними типами та nullable-властивостями
  • lazy може викликати витоки пам’яті в Android при захопленні контексту в замиканні
  • Вибирайте lateinit для змінюваних властивостей і lazy для одноразово ініціалізованих val-залежностей

Ми розробимо мобільний застосунок під ключ

IT Sectr створює застосунки для iOS та Android для стартапів і бізнесу з 2017 року. Ми проконсультуємо вас і запропонуємо найкраще рішення.

Обговорити проект

Читайте також