1. 概要

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

また、 lateinit キーワードを確認します。これにより、コンパイラーをだまして、コンストラクターではなく、クラスの本体でnull以外のフィールドを初期化できます。

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

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

「レイジー初期化」の概念は、オブジェクトの不要な初期化を防ぐために設計されました。 Javaでは、怠惰でスレッドセーフな方法でオブジェクトを作成するのは簡単なことではありません。 シングルトンのようなパターンには、マルチスレッド、テストなどに重大な欠陥があります。 –そしてそれらは今や避けるべきアンチパターンとして広く知られています。

または、Javaの内部オブジェクトの静的初期化を利用して、怠惰を実現することもできます。

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 クラスが読み込まれ、[X192X ]ClassWithHeavyInitializationが作成されます。 次に、インスタンスは static final INSTANCE参照に割り当てられます。

getInstance()が呼び出されるたびに、同じインスタンスを返すことをテストできます。

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

    // then
    assertTrue(classWithHeavyInitialization == classWithHeavyInitialization2);
}

これは技術的には問題ありませんが、もちろんこのような単純な概念には少し複雑すぎます。

3. Kotlinでのレイジー初期化

Javaで遅延初期化パターンを使用するのは非常に面倒であることがわかります。 目標を達成するには、多くの定型コードを作成する必要があります。 幸いなことに、Kotlin言語にはレイジー初期化のサポートが組み込まれています

最初のアクセス時に初期化されるオブジェクトを作成するには、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へのアクセスにより、以前に初期化されたオブジェクトが返されました。

LazyThreadSafetyModelazy関数の引数として渡すことができます。 デフォルトの公開モードはSYNCHRONIZEDです。これは、指定されたオブジェクトを初期化できるのは1つのスレッドのみであることを意味します。

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許容でないクラスプロパティはすべて、コンストラクターで、または変数宣言の一部として初期化する必要があります。 これを行わないと、Kotlinコンパイラはエラーメッセージについて文句を言います。

Kotlin: Property must be initialized or be abstract

これは基本的に、変数を初期化するか、abstractとしてマークする必要があることを意味します。

一方、依存性注入などによって変数を動的に割り当てることができる場合もあります。

変数の初期化を延期するために、フィールドが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)
}

非プリミティブデータ型でのみlateinit変数を使用できることを言及する価値があります。したがって、次のようなものを書くことはできません。

lateinit var value: Int

そうすると、コンパイルエラーが発生します。

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

5. 結論

このクイックチュートリアルでは、オブジェクトの遅延初期化について説明しました。

最初に、Javaでスレッドセーフなレイジー初期化を作成する方法を見ました。 非常に面倒で、多くの定型コードが必要であることがわかりました。

次に、プロパティの遅延初期化に使用されるKotlin lazyキーワードについて詳しく説明しました。 最後に、lateinitキーワードを使用して変数の割り当てを延期する方法を確認しました。

これらすべての例とコードスニペットの実装は、GitHubにあります。