Мързелива инициализация в Котлин

1. Общ преглед

В тази статия ще разгледаме една от най-интересните функции в синтаксиса на Kotlin - мързелива инициализация.

Ще разгледаме и ключовата дума lateinit , която ни позволява да подвеждаме компилатора и да инициализираме ненулеви полета в тялото на класа - вместо в конструктора.

2. Модел за мързелива инициализация в Java

Понякога трябва да конструираме обекти, които имат тромав процес на инициализация. Също така, често не можем да сме сигурни, че обектът, за който сме платили разходите за инициализация в началото на нашата програма, изобщо ще бъде използван в нашата програма.

Концепцията за „мързелива инициализация“ е предназначена да предотврати ненужната инициализация на обекти . В Java създаването на обект по мързелив и безопасен за нишки начин не е лесно да се направи. Модели като Singleton имат значителни недостатъци при многопоточност, тестване и т.н. - и сега те са широко известни като анти-модели, които трябва да се избягват.

Като алтернатива можем да използваме статичната инициализация на вътрешния обект в Java, за да постигнем мързел:

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

Забележете как, само когато ще извикаме метода getInstance () на ClassWithHeavyInitialization , статичният клас LazyHolder ще бъде зареден и ще бъде създаден новият екземпляр на ClassWithHeavyInitialization . След това екземплярът ще бъде присвоен на статичната окончателна справка INSTANCE .

Можем да тестваме, че getInstance () връща същия екземпляр всеки път, когато е извикан:

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

Това технически е добре, но разбира се е твърде сложно за такава проста концепция .

3. Мързелива инициализация в Котлин

Виждаме, че използването на ленивия модел за инициализация в Java е доста тромаво. Трябва да напишем много шаблонни кодове, за да постигнем целта си. За щастие, езикът Kotlin има вградена поддръжка за мързелива инициализация .

За да създадем обект, който ще бъде инициализиран при първия достъп до него, можем да използваме мързеливия метод:

@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) }

Както виждаме, ламбдата, предадена на мързеливата функция, беше изпълнена само веднъж.

Когато имаме достъп до lazyValue за първи път - настъпи действителна инициализация и върнатият екземпляр на класа ClassWithHeavyInitialization беше присвоен на референцията lazyValue . Последващият достъп до lazyValue върна предварително инициализирания обект.

Можем да предадем LazyThreadSafetyMode като аргумент на мързеливата функция. Режимът на публикация по подразбиране е СИНХРОНИЗИРАН , което означава, че само една нишка може да инициализира дадения обект.

Можем да предадем ПУБЛИКАЦИЯ като режим - което ще доведе до това, че всяка нишка може да инициализира дадено свойство. Обектът, присвоен на препратката, ще бъде първата върната стойност - така че първата нишка печели.

Нека да разгледаме този сценарий:

@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) }

Можем да видим, че стартирането на две нишки едновременно води до инициализация на ClassWithHeavyInitialization да се случи два пъти.

Има и трети режим - NONE - но той не трябва да се използва в многонишковата среда, тъй като поведението му е неопределено.

4. Латинитът на Котлин

В Kotlin всяко свойство на клас, което не може да се обезсилва, което е декларирано в класа, трябва да бъде инициализирано или в конструктора, или като част от декларацията на променливата. Ако не успеем да направим това, тогава компилаторът Kotlin ще се оплаче със съобщение за грешка:

Kotlin: Property must be initialized or be abstract

Това по същество означава, че трябва или да инициализираме променливата, или да я маркираме като абстрактна .

От друга страна, има някои случаи, в които променливата може да бъде присвоена динамично, например чрез инжектиране на зависимост.

За да отложим инициализирането на променливата, можем да посочим, че полето е lateinit . Информираме компилатора, че тази променлива will ще бъде присвоена по-късно и освобождаваме компилатора от отговорността да се увери, че тази променлива се инициализира:

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

Ако забравим да инициализираме свойството lateinit , ще получим UninitializedPropertyAccessException :

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

Струва си да се спомене, че можем да използваме lateinit променливи само с непримитивни типове данни. Следователно не е възможно да напишете нещо подобно:

lateinit var value: Int

И ако го направим, ще получим грешка при компилацията:

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

5. Заключение

В този бърз урок разгледахме мързеливата инициализация на обекти.

Първо, видяхме как да създадем нишка-безопасна мързелива инициализация в Java. Видяхме, че това е много тромаво и се нуждае от много примерни кодове.

След това, ние се ровят в Kotlin мързелив ключова дума, която се използва за мързеливи инициализация на имоти. В крайна сметка видяхме как да отложим задаването на променливи с помощта на ключовата дума lateinit .

Прилагането на всички тези примери и кодови фрагменти може да бъде намерено в GitHub.