Kotlinの遅延初期化

Kotlinの遅延初期化

1. 概要

この記事では、Kotlin構文の最も興味深い機能の1つであるレイジー初期化について説明します。

また、コンパイラをだまして、コンストラクタではなくクラスの本体でnull以外のフィールドを初期化できるlateinitキーワードについても見ていきます。

2. Javaでの遅延初期化パターン

面倒な初期化プロセスを持つオブジェクトを作成する必要がある場合があります。 また、多くの場合、プログラムの開始時に初期化のコストを支払ったオブジェクトがプログラムでまったく使用されるかどうかを確認できません。

‘lazy initialization' was designed to prevent unnecessary initialization of objectsの概念。 Javaでは、怠zyでスレッドセーフな方法でオブジェクトを作成するのは簡単なことではありません。 Singletonのようなパターンには、マルチスレッド、テストなどに重大な欠陥があります。 –そして今では避けるべきアンチパターンとして広く知られています。

または、Javaの内部オブジェクトの静的初期化を活用して、怠lazを実現できます。

public class ClassWithHeavyInitialization {

    private ClassWithHeavyInitialization() {
    }

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

    public static ClassWithHeavyInitialization getInstance() {
        return LazyHolder.INSTANCE;
    }
}

ClassWithHeavyInitializationgetInstance()メソッドを呼び出す場合にのみ、静的な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関数に渡されたラムダは1回だけ実行されました。

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

2つのスレッドを同時に開始すると、ClassWithHeavyInitializationの初期化が2回発生することがわかります。

3番目のモード–NONE –もありますが、動作が定義されていないため、マルチスレッド環境では使用しないでください。

4. Kotlinの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プロジェクトであるため、そのままインポートして実行するのは簡単です。