Одложена иницијализација (lazy initialization) — механизам у Kotlin-у при којем се својство објекта иницијализује не у тренутку креирања, већ при првом приступу њему. Према подацима JetBrains, 2024, lateinit и lazy — два уграђена алата за реализацију ове стратегије. Оба решавају проблем одлагања иницијализације, али се принципијелно разликују по механизму рада и области примене.
Главно
Одложена иницијализација — то је образац при којем својство класе добија вредност не у тренутку конструисања објекта, већ касније, на захтев. У Kotlin-у овај образац је реализован на два принципијелно различита начина: модификатором lateinit и делегатом lazy.
Оба механизма решавају заједнички проблем — својство мора да постоји у класи, али његова вредност или још није позната у тренутку креирања објекта, или је њено израчунавање превише ресурсно захтевно за извођење без потребе. Према подацима Google I/O 2023, до 40% својстава у типичној Android апликацији може се оптимизовати кроз одложену иницијализацију, што смањује време покретања за 15–25%.
Избор између lateinit и lazy одређују три фактора: променљивост својства (var или val), време његовог живота (једнократно или вишекратно додељивање) и захтеви за безбедност нити (једнонитни или више nitni приступ).
Први и најчешћи сценарио — 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() или за креирање Retrofit клијената. Међутим, будите опрезни: ако 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. године. Саветоваћемо вас и предложити најбоље решење.
Прочитајте такође