What is it, naokirin?

Kotlinの初期化について今さらまとめておく

前回は、コルーチンについてをまとめました。

naokirin.hatenablog.com

今回は初期化についてをまとめておきます。

初期化の順序

突然ですが、まずは各初期化処理がどの順序で呼ばれるのかを把握しておきます。

class User(val name: String, val age: Int) {
    val country = "Japan"
    var town = "Tokyo"
    var active = false
    init {
        println("initialize...")
        active = true
    }

    constructor(_name: String): this(_name, 0) {
        town = "Osaka"
    }
}

このままではどの順序になるか不明ですが、バイトコードからJavaに変換した結果を見るとおおよそ把握できます。

// ... 中略 ...

public final class User {
   @NotNull
   private final String country;
   @NotNull
   private String town;
   private boolean active;
   @NotNull
   private final String name;
   private final int age;

   // ... 中略 ...

   public User(@NotNull String name, int age) {
      Intrinsics.checkNotNullParameter(name, "name");
      super();
      this.name = name;
      this.age = age;
      this.country = "Japan";
      this.town = "Tokyo";
      String var3 = "initialize...";
      boolean var4 = false;
      System.out.println(var3);
      this.active = true;
   }

   public User(@NotNull String _name) {
      Intrinsics.checkNotNullParameter(_name, "_name");
      this(_name, 0);
      this.town = "Osaka";
   }
}

このコードを見る限りでは以下の順序になっています。

  1. プライマリコンストラクタで定義したプロパティ(インラインプロパティ)
  2. クラス定義のプロパティ代入
  3. init ブロック
  4. セカンダリプロパティでの代入

これをもとに各初期化についてをまとめていきます。

コンストラク

まずはコンストラクタを見ていきます。

Kotlinでは2つのコンストラクタがあり、それぞれプライマリコンストラクタ、セカンダリコンストラクタと呼ばれます。

プライマリコンストラク

プライマリコンストラクは、インスタンスの初期化に利用する値を引数として受け取り、インスタンスの初期化をすることができます。

class User(_name: String, _age: Int) {
    val name: String = _name
    val age: Int = _age
}

単に引数として受け取ることもできますが、規定のgetter, setter を利用する場合は、プライマリコンストラクタで定義までできます。

class User(val name: String, val age: Int) { }

セカンダリコンストラク

セカンダリコンストラクを利用すると、プライマリコンストラクタとは別の構築方法を提供することができます。

class User(val name: String, val age: Int) {

    constructor(_name: String) : this(_name, 0)
}

セカンダリコンストラクタはクラス定義で constructor() で定義します。

セカンダリコンストラクタは、constructor(...) : this(...) のようにして、プライマリコンストラクタか、他のセカンダリコンストラクタを呼び出す必要があります。

セカンダリコンストラクタは初期化ロジックを別途持つことができます。

class User(val name: String, val age: Int) {

    constructor(_name: String) : this(_name, 0) {
        println("Call secondary constructor!")
    }
}

初期化ブロック

Kotlinでは、コンストラクタ以外にもクラス定義で初期化を行うことができます。その1つが初期化ブロックです。

class User(val name: String, val age: Int) {
    init {
        println("Call init block!")
        require(age >= 0, { "age must be positive!" })
    }
}

プロパティの初期化

ここまでに紹介した以外に、クラス定義でプロパティを初期化することもできます。

import java.util.Date

class User(val name: String, val age: Int) {
    val createdDate: Date = Date()
}

プロパティの初期化の際にはクラスに定義された関数を呼び出すこともできます。

遅延される初期化

Kotlinでは null を許容するかどうかを厳密に判断するため、最初にオブジェクトが取得できず、初期化できないから、 null を入れておくことは得策ではありません。

そこで、Kotlinでは遅延初期化の機能があります。

lateinit

lateinit を利用することで「後から使用する前に初期化するので、null非許容で定義します」という宣言をすることができます。

lateinitvar に使用することができます。

class User(val name: String, val age: Int) {
    lateinit var id: Int
}

初期化さえしていれば、この id は必ず null でないとして呼び出しすることができます。

また、初期化されているかを、 isInitialized で確認することができます。ただし、 isInitialized を多用する必要があるのであれば、素直に null許容型にするほうが良いかと思います。

デリゲート(委譲)

デリゲート(委譲)を利用することで、他のインスタンスに処理を任せることができます。

import kotlin.reflect.KProperty

class Delegate {
    operator fun getValue(thisRef: Any?, property: KProperty<*>): String {
        return "call to get ${property.name}!"
    }

    operator fun setValue(thisRef: Any?, property: KProperty<*>, value: String) {
        println("call to set ${property.name} with ${value}!")
    }
}

class User(val name: String, val age: Int) {
    var delegated by Delegate()
}

さらに標準ライブラリには、デリゲートが用意されており、有名なものとしては遅延呼び出しを行える Lazy<T> があります。これは lazy { ... } で生成できるため、以下のようにして遅延初期化に使用されます。

class User(val name: String, val age: Int) {
    val delegated by lazy {
        println("lazy initialized!")
        "hello"
    }
}

インスタンス生成だけでは、この lazy のブロックは呼ばれず、実際にプロパティにアクセスして初めて呼び出されます。

I/Oなどの重い処理を遅延初期化させることで、呼び出すまで遅延させることができます。

また、遅延初期化したプロパティは一度呼び出されると、最初に実行した結果がキャッシュされ、二回目以降はそのキャッシュされたデータが返されるようになります。

まとめ

Kotlinでは多くの初期化処理が存在しているので、初期化順にも気をつけて、上手に使い分けていきたいですね。