Faule Initialisierung in Kotlin

1. Übersicht

In diesem Artikel werden wir uns eine der interessantesten Funktionen der Kotlin-Syntax ansehen - die verzögerte Initialisierung.

Wir werden uns auch das Schlüsselwort lateinit ansehen , mit dem wir den Compiler austricksen und Nicht-Null-Felder im Hauptteil der Klasse initialisieren können - anstatt im Konstruktor.

2. Lazy Initialization Pattern in Java

Manchmal müssen wir Objekte erstellen, die einen umständlichen Initialisierungsprozess haben. Oft können wir auch nicht sicher sein, ob das Objekt, für das wir die Initialisierungskosten zu Beginn unseres Programms bezahlt haben, überhaupt in unserem Programm verwendet wird.

Das Konzept der "verzögerten Initialisierung" wurde entwickelt, um eine unnötige Initialisierung von Objekten zu verhindern . In Java ist es nicht einfach, ein Objekt faul und threadsicher zu erstellen. Muster wie Singleton weisen erhebliche Fehler beim Multithreading, Testen usw. auf - und sie sind heute allgemein als zu vermeidende Anti-Muster bekannt.

Alternativ können wir die statische Initialisierung des inneren Objekts in Java nutzen, um Faulheit zu erreichen:

public class ClassWithHeavyInitialization { private ClassWithHeavyInitialization() { } private static class LazyHolder { public static final ClassWithHeavyInitialization INSTANCE = new ClassWithHeavyInitialization(); } public static ClassWithHeavyInitialization getInstance() { return LazyHolder.INSTANCE; } }

Beachten Sie, dass nur dann, wenn wir die Methode getInstance () für ClassWithHeavyInitialization aufrufen , die statische LazyHolder- Klasse geladen und die neue Instanz der ClassWithHeavyInitialization erstellt wird. Als nächstes wird die Instanz der statischen endgültigen INSTANCE- Referenz zugewiesen .

Wir können testen, ob getInstance () bei jedem Aufruf dieselbe Instanz zurückgibt :

@Test public void giveHeavyClass_whenInitLazy_thenShouldReturnInstanceOnFirstCall() { // when ClassWithHeavyInitialization classWithHeavyInitialization = ClassWithHeavyInitialization.getInstance(); ClassWithHeavyInitialization classWithHeavyInitialization2 = ClassWithHeavyInitialization.getInstance(); // then assertTrue(classWithHeavyInitialization == classWithHeavyInitialization2); }

Das ist technisch in Ordnung, aber für ein so einfaches Konzept natürlich etwas zu kompliziert .

3. Faule Initialisierung in Kotlin

Wir können sehen, dass die Verwendung des verzögerten Initialisierungsmusters in Java ziemlich umständlich ist. Wir müssen viel Code schreiben, um unser Ziel zu erreichen. Glücklicherweise hat die Kotlin-Sprache eine integrierte Unterstützung für die verzögerte Initialisierung .

Um ein Objekt zu erstellen, das beim ersten Zugriff darauf initialisiert wird, können Sie die Lazy- Methode verwenden:

@Test fun givenLazyValue_whenGetIt_thenShouldInitializeItOnlyOnce() { // given val numberOfInitializations: AtomicInteger = AtomicInteger() val lazyValue: ClassWithHeavyInitialization by lazy { numberOfInitializations.incrementAndGet() ClassWithHeavyInitialization() } // when println(lazyValue) println(lazyValue) // then assertEquals(numberOfInitializations.get(), 1) }

Wie wir sehen können, wurde das an die Lazy- Funktion übergebene Lambda nur einmal ausgeführt.

Wenn wir zum ersten Mal auf den LazyValue zugreifen, ist eine tatsächliche Initialisierung aufgetreten , und die zurückgegebene Instanz der ClassWithHeavyInitialization- Klasse wurde der LazyValue- Referenz zugewiesen . Der nachfolgende Zugriff auf den LazyValue gab das zuvor initialisierte Objekt zurück.

Wir können den LazyThreadSafetyMode als Argument an die Lazy- Funktion übergeben. Der Standardveröffentlichungsmodus ist SYNCHRONIZED , was bedeutet, dass nur ein einzelner Thread das angegebene Objekt initialisieren kann.

Wir können eine PUBLICATION als Modus übergeben - was dazu führt, dass jeder Thread eine bestimmte Eigenschaft initialisieren kann. Das der Referenz zugewiesene Objekt ist der erste zurückgegebene Wert - der erste Thread gewinnt also.

Schauen wir uns dieses Szenario an:

@Test fun whenGetItUsingPublication_thenCouldInitializeItMoreThanOnce() { // given val numberOfInitializations: AtomicInteger = AtomicInteger() val lazyValue: ClassWithHeavyInitialization by lazy(LazyThreadSafetyMode.PUBLICATION) { numberOfInitializations.incrementAndGet() ClassWithHeavyInitialization() } val executorService = Executors.newFixedThreadPool(2) val countDownLatch = CountDownLatch(1) // when executorService.submit { countDownLatch.await(); println(lazyValue) } executorService.submit { countDownLatch.await(); println(lazyValue) } countDownLatch.countDown() // then executorService.awaitTermination(1, TimeUnit.SECONDS) executorService.shutdown() assertEquals(numberOfInitializations.get(), 2) }

Wir können sehen, dass das gleichzeitige Starten von zwei Threads dazu führt, dass die Initialisierung der ClassWithHeavyInitialization zweimal erfolgt.

Es gibt auch einen dritten Modus - NONE - aber er sollte nicht in der Multithread-Umgebung verwendet werden, da sein Verhalten undefiniert ist.

4. Kotlins Lateinit

In Kotlin sollte jede nicht nullfähige Klasseneigenschaft, die in der Klasse deklariert ist, entweder im Konstruktor oder als Teil der Variablendeklaration initialisiert werden. Wenn wir das nicht tun, beschwert sich der Kotlin-Compiler mit einer Fehlermeldung:

Kotlin: Property must be initialized or be abstract

Dies bedeutet im Grunde, dass wir die Variable entweder initialisieren oder als abstrakt markieren sollten .

Andererseits gibt es einige Fälle, in denen die Variable dynamisch zugewiesen werden kann, beispielsweise durch Abhängigkeitsinjektion.

Um die Initialisierung der Variablen zu verschieben, können Sie angeben, dass ein Feld lateinit ist . Wir informieren den Compiler, dass diese Willensvariable später zugewiesen wird, und wir befreien den Compiler von der Verantwortung, sicherzustellen, dass diese Variable initialisiert wird:

lateinit var a: String @Test fun givenLateInitProperty_whenAccessItAfterInit_thenPass() { // when a = "it" println(a) // then not throw }

Wenn wir vergessen, die Eigenschaft lateinit zu initialisieren , erhalten wir eine UninitializedPropertyAccessException :

@Test(expected = UninitializedPropertyAccessException::class) fun givenLateInitProperty_whenAccessItWithoutInit_thenThrow() { // when println(a) }

Erwähnenswert ist, dass wir nur lateinit- Variablen mit nicht primitiven Datentypen verwenden können. Daher ist es nicht möglich, so etwas zu schreiben:

lateinit var value: Int

Und wenn wir dies tun, erhalten wir einen Kompilierungsfehler:

Kotlin: 'lateinit' modifier is not allowed on properties of primitive types

5. Schlussfolgerung

In diesem kurzen Tutorial haben wir uns mit der verzögerten Initialisierung von Objekten befasst.

Zunächst haben wir gesehen, wie eine thread-sichere verzögerte Initialisierung in Java erstellt wird. Wir haben gesehen, dass es sehr umständlich ist und viel Code für das Boilerplate benötigt.

Als nächstes haben wir uns mit dem faulen Schlüsselwort Kotlin befasst , das für die verzögerte Initialisierung von Eigenschaften verwendet wird. Am Ende haben wir gesehen, wie die Zuweisung von Variablen mit dem Schlüsselwort lateinit verschoben werden kann .

Die Implementierung all dieser Beispiele und Codefragmente finden Sie auf GitHub.