Android
Kotlin

Kotlin : 'notNull delegate' vs 'lateinit'

More than 1 year has passed since last update.

はじめに

Kotlinには、プロパティを遅延して初期化する際に主に2つの方法があります。Delegation Propertyとlateinitです。
lateinitは、KotlinのM13から追加された機能で、Delegation Propertyよりも後に追加された機能となります。

この2つの機能の使い所は、似ていて。
constructorよりも後に、Nullを許可しない変数への代入を行いたい場合に使用されます。
この記事では、この2つの機能について何が違うのかと、使い所を検証していきます。

デコンパイルによる検証

Kotlinのコードが、JVM上でどのように扱われているのかデコンパイルして確認をします。
以下のような、それぞれの値の初期化を使用した変数を2つもったクラスを作成し、デコンパイルを行います。
※実行する部分は、省いています。

PreCompiled.kt
class Main {

    var delegatesValue: String by Delegates.notNull()
    lateinit var lateinitValue: String

    fun setValues() {
        delegatesValue = "Delegates.notNull"
        lateinitValue = "lateinit"
    }
}

こちらが、デコンパイルしたコードになります。
何個かポイントをあげて解説を行います。

まずDelegates.notNullはReadWritePropertyなため、裏ではReadWritePropertyとして宣言されます。そして、ReadWritePropertyとして宣言されたものはKPropertyという形でReflectionを使用してアクセスされます。
次に、lateinitを使用したものは、Javaの標準の宣言と同じく宣言されます。

Decompiled.java
public final class Main {
   @NotNull
   private final ReadWriteProperty delegatesValue$delegate;
   @NotNull
   public String lateinitValue;
   // $FF: synthetic field
   static final KProperty[] $$delegatedProperties = new KProperty[]{(KProperty)Reflection.mutableProperty1(new MutablePropertyReference1Impl(Reflection.getOrCreateKotlinClass(Main.class), "delegatesValue", "getDelegatesValue()Ljava/lang/String;"))};

   @NotNull
   public final String getDelegatesValue() {
      return (String)this.delegatesValue$delegate.getValue(this, $$delegatedProperties[0]);
   }

   public final void setDelegatesValue(@NotNull String var1) {
      Intrinsics.checkParameterIsNotNull(var1, "<set-?>");
      this.delegatesValue$delegate.setValue(this, $$delegatedProperties[0], var1);
   }

   @NotNull
   public final String getLateinitValue() {
      String var10000 = this.lateinitValue;
      if(this.lateinitValue == null) {
         Intrinsics.throwUninitializedPropertyAccessException("lateinitValue");
      }

      return var10000;
   }

   public final void setLateinitValue(@NotNull String var1) {
      Intrinsics.checkParameterIsNotNull(var1, "<set-?>");
      this.lateinitValue = var1;
   }

   public final void setValues() {
      this.setDelegatesValue("Delegates.notNull");
      this.lateinitValue = "lateinit";
   }

   public Main() {
      this.delegatesValue$delegate = Delegates.INSTANCE.notNull();
   }

}

この結果を見ると、Delegatesの方がReflectionを使用している分、重い処理をしています。

どちらを使えばいいのか

結局、知りたいことはDelegates.notNullとlateinitどちらを使うのがいいかということだと思います。
結論から言えば、大抵の場合はlateinitで事足りますし、lateinitを使ったほうがいいと思いました。
では、なぜDelegates.notNullが存在するのかというと、その答えはKotlinのフォーラムの以下の回答の通りです。

https://discuss.kotlinlang.org/t/notnull-delegate-vs-lateinit/1923

簡潔に訳すと、

  • notNullを使用したものは、各プロパティについてごく小さい追加のオブジェクトを定義します。ただし、数が多くなると無視できない差を生みます。
  • 外部からのJava Fieldへのインジェクション(DIなど)にnotNullを使用できません。
  • 値型(Int, Long, etc)には、lateinitを使用できません。

つまり、まとめると。

lateinit notNull
値型
オブジェクト型 △(ただし、DIなどは☓)

ということになります。
lateinitとnotNull、用法を守って使っていきましょう!

おまけ

エラーの違い

それぞれ、値が入っていない際にアクセスした際のエラーについて見てみます。

未初期化のnotNullプロパティに対して、アクセスした場合のエラー

java.lang.IllegalStateException: Property delegatesValue should be initialized before get.

未初期化のlateinitプロパティに対して、アクセスした場合のエラー

kotlin.UninitializedPropertyAccessException: lateinit property lateinitValue has not been initialized

ということで、notNullを使用した場合はjava.lang.IllegalStateExceptionに、lateinitを使用した場合はkotlin.UninitializedPropertyAccessExceptionとなります。

参考文献