Odložená inicializace (lazy initialization) — mechanismus v Kotlin, při kterém se vlastnost objektu inicializuje ne v okamžiku vytvoření, ale při prvním přístupu k ní. Podle údajů JetBrains, 2024 jsou lateinit a lazy dva vestavěné nástroje pro implementaci této strategie. Oba řeší problém odložení inicializace, ale zásadně se liší mechanismem fungování a oblastí použití.
Hlavní
Odložená inicializace — je vzor, při kterém vlastnost třídy získává hodnotu ne v okamžiku konstruování objektu, ale později, na vyžádání. V Kotlin je tento vzor implementován dvěma zásadně odlišnými způsoby: modifikátorem lateinit a delegátem lazy.
Oba mechanismy řeší společný problém — vlastnost musí existovat ve třídě, ale její hodnota buď není v okamžiku vytvoření objektu ještě známa, nebo je její výpočet příliš náročný na provedení bez potřeby. Podle údajů Google I/O 2023 lze až 40% vlastností v typické aplikaci pro Android optimalizovat pomocí odložené inicializace, což snižuje dobu spouštění o 15–25%.
Výběr mezi lateinit a lazy určují tři faktory: měnlivost vlastnosti (var nebo val), doba jejího života (jednorázové nebo vícenásobné přiřazení) a požadavky na thread-safe (jednovláknový nebo vícevláknový přístup).
První a nejčastější scénář — Dependency Injection. Framework (Dagger, Hilt, Koin) vkládá závislosti po vytvoření objektu, proto vlastnost nemůže být inicializována v konstruktoru. Bez lateinit bychom museli deklarovat všechny závislosti jako nullable a kontrolovat je při každém použití.
Druhý scénář — těžké zdroje: databáze, síťový klient, správce souborů. Jejich vytvoření vyžaduje čas a paměť, proto by měly být inicializovány pouze při skutečném použití. lazy je pro takové případy ideální, garantuje jednorázové vytvoření.
Třetí situace — komponenty Android (Activity, Fragment, ViewModel), jejichž životní cyklus je řízen operačním systémem. Vlastnosti závislé na onCreate, onViewCreated nebo init bloku ViewModel nemohou být inicializovány v konstruktoru.
lateinit — je modifikátor pro var-vlastnosti, který umožňuje kompilátoru Kotlin odložit inicializaci. Kompilátor nevyžaduje přiřazení hodnoty v konstruktoru, ale generuje runtime kontrolu při každém přístupu: pokud vlastnost není inicializována, je vyhozena UninitializedPropertyAccessException.
class MainActivity {
lateinit var binding: ActivityMainBinding
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivityMainBinding.inflate(layoutInflater)
setContentView(binding.root)
}
}
Omezení lateinit: vlastnost musí být deklarována jako var (ne val), non-nullable, ne primitivního typu (Int, Double, Boolean atd.). Důvod — primitivní typy jsou kompilovány do JVM primitiv, která nemají stav «není inicializováno». Pro nullable vlastnosti není odložená inicializace potřeba: null již znamená absenci hodnoty.
Pro kontrolu stavu lateinit vlastnosti se používá vestavěný odkaz přes operátor ::: ::propertyName.isInitialized. Toto je jediný bezpečný způsob, jak zkontrolovat, zda je vlastnost inicializována, bez rizika získání výjimky. Kontrola je dostupná pouze ze stejné třídy nebo vnitřní třídy, nikoli z vnějšího kódu.
class LoginFragment {
lateinit var binding: FragmentLoginBinding
fun isReady(): Boolean {
return ::binding.isInitialized
}
}
lateinit nepřidává režii po inicializaci: po přiřazení hodnoty je přístup k vlastnosti identický s přímým odkazem na pole. Jediným nákladem je kontrola inicializace při každém čtení před přiřazením. Po inicializaci JIT kompilátor kontrolu optimalizuje.
Důležitá vlastnost: lateinit vlastnosti nelze použít v inline třídách a nejsou podporovány pro vlastnosti s vlastními getter/setter. Pokud vlastnost vyžaduje vypočítaný přístup — použijte lazy místo lateinit.
lazy — je delegát vlastnosti, vestavěný do standardní knihovny Kotlin. Vypočítá hodnotu při prvním přístupu k vlastnosti a kešuje výsledek pro všechna následující volání. Na rozdíl od lateinit, lazy funguje pouze s val, čímž činí vlastnost po inicializaci neměnnou.
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 přijímá volitelný parametr LazyThreadSafetyMode, který řídí mechanismus thread-safe. Ve výchozím nastavení se používá SYNCHRONIZED — dvojitá kontrola s uzamčením (Double-Checked Locking), garantující jednorázovou inicializaci i při současném přístupu z více vláken.
Režim PUBLICATION umožňuje paralelní inicializaci: několik vláken může současně provést inicializační blok, ale výsledek bude přijat pouze od prvního dokončeného. Je to rychlejší než SYNCHRONIZED při vysoké konkurenci, ale zvyšuje spotřebu zdrojů.
Režim NONE zcela vypíná synchronizaci. Použijte jej pouze pro vlastnosti, k nimž přístup probíhá garantovaně z jednoho vlákna. V tomto režimu lazy pracuje s minimální režií — prakticky jako přímé přiřazení.
val heavyConfig: Config by lazy(LazyThreadSafetyMode.NONE) {
Config.loadFromFile("config.json")
}
lazy je správnou volbou pro jednorázově inicializované závislosti: repozitáře, síťové klienty, cache, databáze. Sémantika val chrání před náhodným přepsáním a výchozí thread-safe činí kód bezpečným ve vícevláknovém prostředí. lazy také správně funguje s primitivními typy, což je u lateinit nemožné.
V Android se lazy často používá pro inicializaci závislostí ViewModel přes by viewModels() nebo pro vytváření Retrofit klientů. Buďte však opatrní: pokud lazy blok zachytí odkaz na Activity nebo Fragment, může to vést k úniku paměti, protože delegát uchovává uzavření až do konce životnosti vlastnosti.
Výběr mezi lateinit a lazy — není otázkou preference, ale architektonické rozhodnutí určené povahou vlastnosti. Každý mechanismus řeší svůj úkol a jejich oblasti použití se pouze částečně překrývají.
| Kritérium | lateinit | lazy |
|---|---|---|
| Typ vlastnosti | pouze var | pouze val |
| Nullable | zakázán | povolen |
| Primitivní typy | zakázány | povoleny |
| Thread-safe | není zaručena | SYNCHRONIZED výchozí |
| Kontrola stavu | ::x.isInitialized | není vyžadována |
| Výjimka při chybě | UninitializedPropertyAccessException | chyba v inicializačním bloku |
| Kešování | nepoužívá se | jednorázový výpočet |
| Android Binding | View Binding, Data Binding | nepoužívá se |
| DI frameworky | Dagger, Hilt, Koin | ruční injektáž |
Použijte lateinit, když se vlastnost má měnit po inicializaci nebo její vytvoření řídí externí kód. Typický příklad — View Binding v Android Activity: binding je vytvořen v onCreate, ale zůstává var, protože framework nepodporuje val pro tento scénář.
Použijte lazy, když je vlastnost inicializována jednorázově, její výpočet je nákladný a hodnota se během života objektu nemění. Klasický příklad — líné vytvoření Retrofit klienta nebo Room databáze při prvním přístupu k repozitáři.
V jedné třídě lze současně používat oba mechanismy. Například lateinit pro View Binding a lazy pro repozitář. To je normální praxe, odrážející různé požadavky na různé vlastnosti. Hlavní — nepleťte sémantiku: nepoužívejte lateinit tam, kde je potřeba val, a nepoužívejte lazy pro vlastnosti, které se mají přepisovat.
Nejčastější chyba s lateinit — přístup k vlastnosti před jejím inicializováním. To vede k UninitializedPropertyAccessException, která není zachycena ve fázi kompilace, protože Kotlin důvěřuje vývojáři ve správném pořadí inicializace. Řešení — vždy kontrolujte stav přes ::property.isInitialized před přístupem v nejednoznačných situacích.
Druhý častý problém — použití lateinit pro vlastnosti, které jsou sémanticky val. Pokud je hodnota nastavena jednou a už se nemění, lazy je správnější volba. Činí vlastnost neměnnou, vylučuje náhodné přepsání a přidává thread-safe zdarma.
Třetí chyba — lazy s vedlejšími účinky. Inicializační blok lazy by neměl měnit vnější stav ani spoléhat na pořadí inicializace jiných lazy vlastností, protože pořadí výpočtů závisí na prvním přístupu a může být nejednoznačné. Pokud lazy vlastnosti odkazují jedna na druhou, vede to k cyklické závislosti a StackOverflowError.
Čtvrtý problém — únik paměti přes lazy v Android. Pokud lazy blok zachytí odkaz na Activity nebo Fragment — delegát uchovává uzavření a garbage collector nemůže uvolnit komponentu ani po jejím zničení. Řešení — používejte lazy pouze s krátkodobými objekty nebo předejte Application kontext, ne Activity.
Pátá typická chyba — pokus o použití lateinit na primitivní typy. Kompilátor Kotlin to blokuje na úrovni syntaxe, ale vývojáři se snaží obejít omezení přes nullable obaly. To vede ke zbytečným kontrolám null a zcela ruší výhody odložené inicializace.
Často kladené otázky
lateinit — modifikátor pro var-vlastnosti, umožňující inicializaci po konstruktoru. lazy — delegát pro val-vlastnosti, vypočítávající hodnotu při prvním přístupu a kešující ji. lateinit nepodporuje primitivní typy a nullable, zatímco lazy je ve výchozím nastavení thread-safe.
Ano, přes vestavěný odkaz na vlastnost: ::propertyName.isInitialized. Metoda vrací true, pokud je vlastnost již inicializována. To je jediný bezpečný způsob, jak se vyhnout UninitializedPropertyAccessException při práci s lateinit poli.
Primitivní typy — Int, Double, Boolean a další — jsou kompilovány do JVM primitiv (int, double, boolean), která nemají stav «není inicializováno». lateinit používá null jako příznak a primitiva nemohou být null, proto je mechanismus pro tyto typy fyzicky nerealizovatelný.
Ve výchozím nastavení se používá LazyThreadSafetyMode.SYNCHRONIZED — dvojitá kontrola s uzamčením, garantující jednorázovou inicializaci při přístupu z více vláken. Pro jednovláknové scénáře použijte NONE, pro vysokou konkurenci — PUBLICATION.
Když se vlastnost má měnit po inicializaci nebo její vytvoření řídí framework. Typický příklad — View Binding v Android Activity: binding je vytvořen v onCreate a musí být var. Pro jednorázově inicializované val závislosti použijte lazy.
Shrnutí
Vyvineme mobilní aplikaci na klíč
IT Sectr vytváří aplikace pro iOS a Android pro startupy a podniky od roku 2017. Poradíme vám a navrhneme nejlepší řešení.
Přečtěte si také