1. 概要

この記事では、Kotlinの init ブロック、プロパティ初期化子、およびコンストラクターの違いについて説明します。

2. サンプルクラス

Kotlinのコンストラクターとinitブロックの違いを理解するために、記事全体で次の例を使用します。

class Person(val firstName: String, val lastName: String) {

    private val fullName: String = "$firstName $lastName".trim()
      .also { println("Initializing full name") }

    init {
        println("You're $fullName")
    }

    private val initials =  "${firstName.firstOrEmpty()}${lastName.firstOrEmpty()}".trim()
      .also { println("Initializing initials") }

    init {
        println("You're initials are $initials")
    }

    constructor(lastName: String): this("", lastName) {
        println("I'm secondary")
    }

    private fun String.firstOrEmpty(): Char = firstOrNull()?.toUpperCase() ?: ' '
}

上記の例では、構築の入力として2つのStringを受け入れるプライマリコンストラクターがあります。 さらに、個人の姓のみを受け入れる2次コンストラクターもあります。 これらの2つのコンストラクターに加えて、コンストラクターの入力に基づいてプロパティ初期化子を使用して2つの変数を宣言しています。 最後に、2つの初期化ブロック(キーワード init が前に付いたブロック)があります。

3. コンストラクターとinitブロック

二次コンストラクターとは対照的に、一次コンストラクターにはコードを含めることはできません。 この制限を克服するために、上記の例で行ったように、初期化ロジックをinitブロックとプロパティ初期化子の中に置くことができます。

インスタンスの初期化中に、Kotlinは初期化ブロックとプロパティ初期化子をクラスbodyに表示されるのと同じ順序で実行します。 したがって、 Person クラスのインスタンスを作成すると、クラスに表示されるのと同じ順序でいくつかのログが表示されます。

val p = Person("ali", "dehghani")

上記のオブジェクトインスタンス化のログは次のようになります。

Initializing full name
You're ali dehghani
Initializing initials
You're initials are AD

上記のように、最初のログは fullName プロパティ初期化子、2番目のログは最初の init ブロック、3番目のログはinitialsプロパティ初期化子です。 、最後の1つは、最後のinitブロック用です。

3.1. バイトコード表現

Kotlinのプライマリコンストラクターに一部のコードを配置することはできませんが、コンストラクター用に生成されたバイトコードにはすべての初期化ロジックが含まれます。 基本的に、 Kotlinコンパイラは、すべてのプロパティ初期化子とinitブロック初期化子からのロジックを含む大きなコンストラクターを生成します。

簡単に言うと、initブロックとプロパティ初期化子は最終的にプライマリコンストラクターの一部になります。 明らかに、生成されたバイトコードを調べることでこれを確認できます。

>> kotlinc Person.kt
>> javap -c -p com.baeldung.initblock.Person
// primary constructor
public com.baeldung.initblock.Person(java.lang.String, java.lang.String);
    Code:
       // primary constructor properties
      13: invokespecial #20                 // Method Object."<init>":()V
      16: aload_0
      17: aload_1
      18: putfield      #23                 // Field firstName:LString;
      21: aload_0
      22: aload_2
      23: putfield      #25                 // Field lastName:LString;
      26: aload_0

      // full name property initializer
      27: new           #27                 // class StringBuilder
      30: dup
      31: invokespecial #28                 // Method StringBuilder."<init>":()V
      34: aload_0
      35: getfield      #23                 // Field firstName:LString;
      38: invokevirtual #32                 // Method StringBuilder.append:(LString;)LStringBuilder;
      41: bipush        32
      43: invokevirtual #35                 // Method StringBuilder.append:(C)LStringBuilder;
      46: aload_0
      47: getfield      #25                 // Field lastName:LString;
      50: invokevirtual #32                 // Method StringBuilder.append:(LString;)LStringBuilder;
      53: invokevirtual #39                 // Method StringBuilder.toString:()LString;

      // printing
      99: ldc           #57                 // String Initializing full name
     101: astore        8
     103: iconst_0
     104: istore        9
     106: getstatic     #63                 // Field System.out:LPrintStream;
     109: aload         8
     111: invokevirtual #69                 // Method PrintStream.println:(LObject;)V
     
     // first init block
     123: putfield      #78                 // Field fullName:LString;
     126: nop
     127: ldc           #80                 // String You\'re
     129: aload_0
     130: getfield      #78                 // Field fullName:LString;
     133: invokestatic  #84                 // Method ntrinsics.stringPlus:(LString;LObject;)LString;
     136: astore_3
     137: iconst_0
     138: istore        4
     140: getstatic     #63                 // Field System.out:LPrintStream;
     143: aload_3
     144: invokevirtual #69                 // Method PrintStream.println:(LObject;)V
     
     // other property initializers and init blocks

これは、バイトコードの高度に切り捨てられ単純化されたバージョンです。 上に示したように、バイトコードの最初の部分は、firstNameおよびlastNameコンストラクタープロパティを初期化しています。 その後、バイトコードは fullName プロパティ初期化子専用になり、静的ログを出力します。 そして最後に、最初のinitブロックを担当する一連のオペコードがあります。

バイトコードの残りの部分は、簡潔にするために切り捨てられます。 とにかく、Kotlinがすべてのロジックを保持するようにプライマリコンストラクターをコンパイルすることは明らかです。

3.2. 二次コンストラクタ

一次コンストラクターとは対照的に、二次コンストラクターには初期化ロジックを含めることができます。 プライマリコンストラクターへの委任は、明示的または暗黙的に、セカンダリコンストラクターの最初のステートメントとして発生します。 したがって、Kotlinは、セカンダリコンストラクターの本体の前に、すべてのイニシャライザーブロックとプロパティイニシャライザーでコードを実行します。

したがって、セカンダリコンストラクターを使用してインスタンスを作成すると、次のようになります。

val p = Person("dehghani")

プライマリコンストラクタからの他のすべてのログの後に、セカンダリコンストラクタログが表示されます。

Initializing full name
You're dehghani
Initializing initials
You're initials are D
I'm secondary

この場合も、バイトコードはセカンダリコンストラクターの呼び出しの順序を確認できます。

public com.baeldung.initblock.Person(java.lang.String);
    Code:
      // calling the primary constructor
      10: invokespecial #109                // Method "<init>":(LString;LString;)V

      // secondary log
      13: ldc           #111                // String I\'m secondary
      18: getstatic     #63                 // Field System.out:LPrintStream;
      21: aload_2
      22: invokevirtual #69                 // Method PrintStream.println:(LObject;)V

上に示したように、最初にプライマリコンストラクターを呼び出し、次に期待されるログを標準出力に出力します。

4. 結論

この記事では、Kotlinのinitブロックとコンストラクターの違いを確認しました。 また、この違いをよりよく理解するために、それぞれの場合に生成されたバイトコードを確認しました。

いつものように、すべての例はGitHubから入手できます。