Ленивая инициализация в Kotlin

Ленивая инициализация в 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, поэтому его должно быть легко импортировать и запускать как есть.