Ленивая инициализация в Kotlin
1. обзор
В этой статье мы рассмотрим одну из самых интересных функций синтаксиса Kotlin - отложенную инициализацию.
Мы также рассмотрим ключевое словоlateinit, которое позволяет нам обмануть компилятор и инициализировать ненулевые поля в теле класса, а не в конструкторе.
2. Шаблон отложенной инициализации в Java
Иногда нам нужно создавать объекты, которые имеют громоздкий процесс инициализации. Кроме того, часто мы не можем быть уверены, что объект, за который мы заплатили стоимость инициализации в начале нашей программы, вообще будет использоваться в нашей программе.
Концепция‘lazy initialization' was designed to prevent unnecessary initialization of objects. В 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. Затем экземпляр будет назначен ссылкеstaticfinalINSTANCE.
Мы можем проверить, чтоgetInstance() возвращает один и тот же экземпляр каждый раз, когда он вызывается:
@Test
public void giveHeavyClass_whenInitLazy_thenShouldReturnInstanceOnFirstCall() {
// when
ClassWithHeavyInitialization classWithHeavyInitialization
= ClassWithHeavyInitialization.getInstance();
ClassWithHeavyInitialization classWithHeavyInitialization2
= ClassWithHeavyInitialization.getInstance();
// then
assertTrue(classWithHeavyInitialization == classWithHeavyInitialization2);
}
Технически это нормально, но, конечно,a little bit too complicated for such a simple concept.
3. Ленивая инициализация в Kotlin
Мы можем видеть, что использование ленивого шаблона инициализации в Java довольно громоздко. Нам нужно написать много шаблонного кода для достижения нашей цели. Luckily, the Kotlin language has built-in support for lazy initialization.
Чтобы создать объект, который будет инициализирован при первом обращении к нему, мы можем использовать методlazy:
@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)
}
Как мы видим, лямбда, переданная в функциюlazy, была выполнена только один раз.
Когда мы впервые обращаемся кlazyValue, произошла фактическая инициализация, и возвращенный экземпляр классаClassWithHeavyInitialization был назначен ссылкеlazyValue. Последующий доступ кlazyValue вернул ранее инициализированный объект.
Мы можем передатьLazyThreadSafetyMode в качестве аргумента функцииlazy. Режим публикации по умолчанию -SYNCHRONIZED, что означает, что только один поток может инициализировать данный объект.
Мы можем передатьPUBLICATION в качестве режима, что приведет к тому, что каждый поток может инициализировать данное свойство. Объект, назначенный ссылке, будет первым возвращаемым значением, поэтому первый поток выигрывает.
Давайте посмотрим на этот сценарий:
@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. Котлинlateinit
В Kotlin каждое свойство класса, не допускающее значения NULL, объявленное в классе, должно быть назначено в конструкторе; в противном случае мы получим ошибку компилятора. С другой стороны, в некоторых случаях переменная может быть назначена динамически, например, путем внедрения зависимости.
Чтобы отложить инициализацию переменной, мы можем указать, что поле -lateinit.. Мы информируем компилятор, что эта переменная будет назначена позже, и мы освобождаем компилятор от ответственности за обеспечение инициализации этой переменной:
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)
}
5. Заключение
В этом быстром уроке мы рассмотрели ленивую инициализацию объектов.
Во-первых, мы увидели, как создать поточно-ориентированную ленивую инициализацию в Java. Мы увидели, что это очень громоздко и требует много стандартного кода.
Затем мы углубились в ключевое слово Kotlinlazy, которое используется для отложенной инициализации свойств. В конце концов, мы увидели, как отложить присвоение переменных с помощью ключевого словаlateinit.
Реализация всех этих примеров и фрагментов кода можно найти вGitHub project - это проект Maven, поэтому его должно быть легко импортировать и запускать как есть.