Kotlinの遅延初期化

1概要

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

また、コンストラクターではなくクラス本体で、コンパイラーをトリックしてNULL以外のフィールドを初期化することを可能にする 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;
    }
}

ClassWithHeavyInitialization getInstance() メソッドを呼び出すときだけ、静的な LazyHolder クラスがロードされ、 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 関数に渡されたラムダは一度だけ実行されました。

初めて lazyValue にアクセスしたとき - 実際の初期化が行われ、返された ClassWithHeavyInitialization クラスのインスタンスが lazyValue 参照に割り当てられました。 lazyValue へのその後のアクセスは、以前に初期化されたオブジェクトを返しました。

lazy 関数の引数として LazyThreadSafetyMode を渡すことができます。デフォルトのパブリケーションモードは 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回行われることがわかります。

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

4コトリンの lateinit

Kotlinでは、クラスで宣言されているすべてのnullを許容しないクラスプロパティはコンストラクタで代入する必要があります。それ以外の場合は、コンパイラエラーが発生します。一方、例えば依存性注入によって変数を動的に割り当てることができる場合がいくつかあります。

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

5結論

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

まず、Javaでスレッドセーフな遅延初期化を作成する方法を見ました。

これは非常に面倒で、多くの定型コードが必要であることがわかりました。

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

これらすべての例とコードスニペットの実装はhttps://github.com/eugenp/tutorials/tree/master/core-kotlin[GitHubプロジェクト]にあります。そのままインポートして実行します。