Leniwa inicjalizacja (lazy initialization) — mechanizm w Kotlin, w którym właściwość obiektu jest inicjalizowana nie w momencie tworzenia, ale przy pierwszym odwołaniu do niej. Według danych JetBrains, 2024, lateinit i lazy to dwa wbudowane narzędzia do realizacji tej strategii. Oba rozwiązują problem opóźnienia inicjalizacji, ale zasadniczo różnią się mechanizmem działania i obszarem zastosowania.
Najważniejsze
Leniwa inicjalizacja — to wzorzec, w którym właściwość klasy otrzymuje wartość nie w momencie konstruowania obiektu, ale później, na żądanie. W Kotlin ten wzorzec jest zrealizowany na dwa zasadniczo różne sposoby: modyfikatorem lateinit i delegatem lazy.
Oba mechanizmy rozwiązują wspólny problem — właściwość musi istnieć w klasie, ale jej wartość albo nie jest jeszcze znana w momencie tworzenia obiektu, albo jej obliczenie jest zbyt zasobożerne, aby wykonywać je bez potrzeby. Według danych Google I/O 2023, aż 40% właściwości w typowej aplikacji Android można zoptymalizować poprzez leniwą inicjalizację, co skraca czas uruchamiania o 15–25%.
Wybór między lateinit a lazy określają trzy czynniki: zmienność właściwości (var lub val), czas jej życia (jednorazowe lub wielokrotne przypisanie) oraz wymagania dotyczące bezpieczeństwa wątkowego (dostęp jedno- lub wielowątkowy).
Pierwszy i najczęstszy scenariusz — Dependency Injection. Framework (Dagger, Hilt, Koin) wstrzykuje zależności po utworzeniu obiektu, dlatego właściwość nie może być zainicjalizowana w konstruktorze. Bez lateinit trzeba byłoby deklarować wszystkie zależności jako nullable i sprawdzać je przy każdym użyciu.
Drugi scenariusz — zasobochłonne zasoby: baza danych, klient sieciowy, menedżer plików. Ich utworzenie wymaga czasu i pamięci, dlatego powinny być inicjalizowane tylko przy rzeczywistym użyciu. lazy idealnie nadaje się do takich przypadków, gwarantując jednorazowe utworzenie.
Trzecia sytuacja — komponenty Androida (Activity, Fragment, ViewModel), których cykl życia jest zarządzany przez system operacyjny. Właściwości zależne od onCreate, onViewCreated lub bloku init ViewModel nie mogą być zainicjalizowane w konstruktorze.
lateinit — to modyfikator dla właściwości var, który pozwala kompilatorowi Kotlin opóźnić inicjalizację. Kompilator nie wymaga przypisania wartości w konstruktorze, ale generuje sprawdzenie w czasie wykonania przy każdym dostępie: jeśli właściwość nie jest zainicjalizowana, zgłaszane jest UninitializedPropertyAccessException.
class MainActivity {
lateinit var binding: ActivityMainBinding
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivityMainBinding.inflate(layoutInflater)
setContentView(binding.root)
}
}
Ograniczenia lateinit: właściwość musi być zadeklarowana jako var (nie val), nie nullable, nie prymitywnego typu (Int, Double, Boolean itd.). Przyczyną — prymitywne typy są kompilowane do JVM-prymitywów, które nie mają stanu «nie zainicjalizowane». Dla właściwości nullable leniwa inicjalizacja nie jest potrzebna: null już oznacza brak wartości.
Do sprawdzenia stanu właściwości lateinit używa się wbudowanego odwołania przez operator ::: ::propertyName.isInitialized. To jedyny bezpieczny sposob sprawdzenia, czy właściwość jest zainicjalizowana, bez ryzyka otrzymania wyjątku. Sprawdzenie jest dostępne tylko z tej samej klasy lub klasy wewnętrznej, nie z zewnętrznego kodu.
class LoginFragment {
lateinit var binding: FragmentLoginBinding
fun isReady(): Boolean {
return ::binding.isInitialized
}
}
lateinit nie dodaje narzutu po inicjalizacji: po przypisaniu wartości dostęp do właściwości jest identyczny z bezpośrednim odwołaniem do pola. Jedynym kosztem jest sprawdzenie inicjalizacji przy każdym odczycie przed przypisaniem. Po inicjalizacji kompilator JIT optymalizuje sprawdzenie.
Ważna cecha: właściwości lateinit nie mogą być używane w klasach inline i nie są obsługiwane dla właściwości z niestandardowymi getter/setter. Jeśli właściwość wymaga dostępu obliczeniowego — użyj lazy zamiast lateinit.
lazy — to delegat właściwości, wbudowany w standardową bibliotekę Kotlin. Oblicza wartość przy pierwszym odwołaniu do właściwości i buforuje wynik dla wszystkich kolejnych wywołań. W przeciwieństwie do lateinit, lazy działa tylko z val, czyniąc właściwość niezmienną po inicjalizacji.
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 przyjmuje opcjonalny parametr LazyThreadSafetyMode, który kontroluje mechanizm bezpieczeństwa wątkowego. Domyślnie używany jest SYNCHRONIZED — podwójne sprawdzenie z blokadą (Double-Checked Locking), gwarantujące jednorazową inicjalizację nawet przy jednoczesnym dostępie z wielu wątków.
Tryb PUBLICATION dopuszcza równoległą inicjalizację: wiele wątków może jednocześnie wykonać blok inicjalizacyjny, ale wynik zostanie przyjęty tylko od pierwszego, który zakończy. Jest to szybsze niż SYNCHRONIZED przy wysokiej konkurencji, ale zwiększa zużycie zasobów.
Tryb NONE całkowicie wyłącza synchronizację. Używaj go tylko dla właściwości, do których dostęp gwarantowanie odbywa się z jednego wątku. W tym trybie lazy działa z minimalnym narzutem — praktycznie jak bezpośrednie przypisanie.
val heavyConfig: Config by lazy(LazyThreadSafetyMode.NONE) {
Config.loadFromFile("config.json")
}
lazy to prawidłowy wybór dla jednorazowo inicjalizowanych zależności: repozytoriów, klientów sieciowych, buforów, baz danych. Semantyka val chroni przed przypadkowym nadpisaniem, a domyślne bezpieczeństwo wątkowe czyni kod bezpiecznym w środowisku wielowątkowym. lazy poprawnie działa również z prymitywnymi typami, co jest niemożliwe w przypadku lateinit.
W Android lazy jest często używany do inicjalizacji zależności ViewModel przez by viewModels() lub do tworzenia klientów Retrofit. Należy jednak zachować ostrożność: jeśli blok lazy przechwytuje referencję do Activity lub Fragment, może to prowadzić do wycieku pamięci, ponieważ delegat przechowuje domknięcie do końca życia właściwości.
Wybór między lateinit a lazy — to nie kwestia preferencji, ale decyzja architektoniczna, określona przez charakter właściwości. Każdy mechanizm rozwiązuje swoje zadanie, a ich obszary zastosowania tylko częściowo się pokrywają.
| Kryterium | lateinit | lazy |
|---|---|---|
| Typ właściwości | tylko var | tylko val |
| Nullable | zabroniony | dozwolony |
| Typy prymitywne | zabronione | dozwolone |
| Bezpieczeństwo wątkowe | nie gwarantowane | SYNCHRONIZED domyślnie |
| Sprawdzenie stanu | ::x.isInitialized | nie wymagane |
| Wyjątek przy błędzie | UninitializedPropertyAccessException | błąd w bloku inicjalizacyjnym |
| Buforowanie | nie stosowane | jednorazowe obliczenie |
| Android Binding | View Binding, Data Binding | nie używane |
| Frameworki DI | Dagger, Hilt, Koin | ręczne wstrzykiwanie |
Używaj lateinit, gdy właściwość powinna zmieniać się po inicjalizacji lub jej utworzenie jest zarządzane przez zewnętrzny kod. Typowy przykład — View Binding w Android Activity: binding jest tworzony w onCreate, ale pozostaje var, ponieważ framework nie obsługuje val dla tego scenariusza.
Używaj lazy, gdy właściwość jest inicjalizowana jednorazowo, jej obliczenie jest kosztowne, a wartość nie zmienia się w ciągu życia obiektu. Klasyczny przykład — leniwe tworzenie klienta Retrofit lub bazy danych Room przy pierwszym odwołaniu do repozytorium.
W jednej klasie mogą być jednocześnie używane oba mechanizmy. Na przykład lateinit dla View Binding i lazy dla repozytorium. To normalna praktyka, odzwierciedlająca różne wymagania wobec różnych właściwości. Najważniejsze — nie mylić semantyki: nie używaj lateinit tam, gdzie potrzebny jest val, i nie używaj lazy dla właściwości, które powinny być nadpisywane.
Najczęstszy błąd z lateinit — odwołanie do właściwości przed jej zainicjalizowaniem. Prowadzi to do UninitializedPropertyAccessException, który nie jest przechwytywany na etapie kompilacji, ponieważ Kotlin ufa programiście w kwestii prawidłowej kolejności inicjalizacji. Rozwiązanie — zawsze sprawdzać stan przez ::property.isInitialized przed dostępem w niejednoznacznych sytuacjach.
Drugi częsty problem — używanie lateinit dla właściwości semantycznie będących val. Jeśli wartość jest ustawiana raz i więcej się nie zmienia, lazy jest lepszym wyborem. Czyni właściwość niezmienną, wyklucza przypadkowe nadpisanie i dodaje bezpieczeństwo wątkowe za darmo.
Trzeci błąd — lazy z efektami ubocznymi. Blok inicjalizacyjny lazy nie powinien zmieniać stanu zewnętrznego ani polegać na kolejności inicjalizacji innych właściwości lazy, ponieważ sekwencja obliczeń zależy od pierwszego odwołania i może być nieoczywista. Jeśli właściwości lazy odwołują się do siebie nawzajem, prowadzi to do cyklicznej zależności i StackOverflowError.
Czwarty problem — wyciek pamięci przez lazy w Android. Jeśli blok lazy przechwytuje referencję do Activity lub Fragment — delegat przechowuje domknięcie, a garbage collector nie może zwolnić komponentu nawet po jego zniszczeniu. Rozwiązanie — używaj lazy tylko z krótkożywymi obiektami lub przekazuj kontekst Application, a nie Activity.
Piąty typowy błąd — próba zastosowania lateinit do typów prymitywnych. Kompilator Kotlin blokuje to na poziomie składni, ale programiści próbują ominąć ograniczenie przez opakowania nullable. Prowadzi to do zbędnych sprawdzeń na null i całkowicie niweluje zalety leniwej inicjalizacji.
Często zadawane pytania
lateinit — modyfikator dla właściwości var, dopuszczający inicjalizację po konstruktorze. lazy — delegat dla właściwości val, obliczający wartość przy pierwszym odwołaniu i buforujący ją. lateinit nie obsługuje typów prymitywnych i nullable, a lazy jest domyślnie bezpieczny wątkowo.
Tak, przez wbudowane odwołanie do właściwości: ::propertyName.isInitialized. Metoda zwraca true, jeśli właściwość jest już zainicjalizowana. To jedyny bezpieczny sposob uniknięcia UninitializedPropertyAccessException podczas pracy z polami lateinit.
Typy prymitywne — Int, Double, Boolean i inne — są kompilowane do JVM-prymitywów (int, double, boolean), które nie mają stanu «nie zainicjalizowane». lateinit używa null jako flagi, a prymitywy nie mogą być null, dlatego mechanizm jest fizycznie nierealizowalny dla tych typów.
Domyślnie używany jest LazyThreadSafetyMode.SYNCHRONIZED — podwójne sprawdzenie z blokadą, gwarantujące jednorazową inicjalizację przy dostępie z wielu wątków. Dla scenariuszy jednowątkowych używaj NONE, dla wysokiej konkurencji — PUBLICATION.
Gdy właściwość powinna zmieniać się po inicjalizacji lub jej utworzenie jest zarządzane przez framework. Typowy przykład — View Binding w Android Activity: binding jest tworzony w onCreate i musi być var. Dla jednorazowo inicjalizowanych zależności val używaj lazy.
Podsumowanie
Opracujemy aplikację mobilną pod klucz
IT Sectr tworzy aplikacje na iOS i Androida dla startupów i firm od 2017 roku. Doradzimy Ci i zaproponujemy najlepsze rozwiązanie.
Przeczytaj również