Einleitung: Das ewige Dilemma des Kotlin-Entwicklers
Wenn Sie jemals in Kotlin programmiert haben und vor der Notwendigkeit standen, die Initialisierung einer Eigenschaft zu verzögern, haben Sie sich sicherlich gefragt: „Was soll ich wählen – by lazy oder lateinit?“ Diese Frage ist ein echter Hit auf Stack Overflow (tausende Aufrufe, dutzende Diskussionen). Und das nicht ohne Grund: Von der richtigen Wahl hängt nicht nur die Lesbarkeit des Codes ab, sondern auch die Leistung der Anwendung.
In diesem Artikel werden wir nicht einfach die Dokumentation nacherzählen. Wir führen eine echte Untersuchung durch: Wir analysieren die interne Funktionsweise beider Mechanismen, vergleichen ihre Geschwindigkeit, ihren Speicherverbrauch und geben klare Empfehlungen. Und am Ende fassen wir mit konkreten Action Items zusammen. Los geht's!
Was sind lateinit und by lazy? Kurze Einführung
Zunächst frischen wir die grundlegenden Konzepte auf. Beide Ansätze lösen eine Aufgabe: die Initialisierung einer Eigenschaft bis zu ihrer ersten Verwendung zu verzögern. Aber sie tun dies auf unterschiedliche Weise.
lateinit var – Ein Versprechen an den Compiler
Das Schlüsselwort lateinit sagt dem Compiler: „Ich verspreche, diese Eigenschaft vor dem ersten Zugriff zu initialisieren, reserviere erstmal nur den Platz dafür.“ Es wird nur mit var und nur für mutable (veränderbare) Eigenschaften verwendet. Ideal geeignet für Dependency Injection (Dagger, Hilt, Koin) oder für Eigenschaften, die in onCreate einer Activity/Fragment initialisiert werden.
class MyActivity : AppCompatActivity() { lateinit var repository: UserRepository
override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) repository = UserRepository() // Initialisierung }
fun loadUser() { // Wenn repository nicht initialisiert ist – wird UninitializedPropertyAccessException ausgelöst repository.getUser() }}by lazy – Fauler Delegat
Der Delegat lazy ist ein flexiblerer Mechanismus. Er erstellt ein Wrapper-Objekt, das beim ersten Zugriff das Lambda aufruft und das Ergebnis speichert. Alle nachfolgenden Zugriffe geben einfach den gespeicherten Wert zurück. Standardmäßig ist er threadsicher (synchronisiert), aber der Modus kann konfiguriert werden.
class MyViewModel : ViewModel() { val heavyData: ExpensiveData by lazy { // Dieser Block wird nur einmal ausgeführt, beim ersten Zugriff ExpensiveData() }}Beide Ansätze lösen das Problem der „faulen Initialisierung“, aber mit unterschiedlichen Kompromissen. Lassen Sie uns diese Kompromisse unter die Lupe nehmen.
Leistung: Wer ist schneller?
Hier kommen wir zum Interessantesten – zu den Zahlen. Leistung ist nicht nur die Zugriffsgeschwindigkeit auf eine Eigenschaft, sondern auch der Overhead für Initialisierung, Speicher und Threadsicherheit.
Geschwindigkeit des ersten Zugriffs
lateinit – der absolute Rekordhalter. Beim ersten Zugriff prüft der Compiler lediglich, ob das Feld nicht null ist (wenn es nullable ist, wird eine Ausnahme ausgelöst). Keine Lambdas, keine Synchronisation. Das ist praktisch kostenlos.
by lazy – langsamer. Beim ersten Zugriff wird ein synchronisierter Block aufgerufen (wenn der Standardmodus LazyThreadSafetyMode.SYNCHRONIZED verwendet wird), das Lambda ausgeführt und das Ergebnis zwischengespeichert. Dies dauert Mikrosekunden, kann aber in stark ausgelasteten Schleifen oder in kritischem Code spürbar sein.
Geschwindigkeit wiederholter Zugriffe
Nach der Initialisierung arbeiten beide Ansätze gleich schnell. lateinit liest einfach das Feld (wie ein normales var). by lazy liest nach der ersten Initialisierung ebenfalls den zwischengespeicherten Wert. Der Geschwindigkeitsunterschied ist in dieser Phase vernachlässigbar.
Speicherverbrauch
Hier gewinnt lateinit klar. Es erzeugt kei