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.