1. 概要

Kotlinの文字列補間を使用すると、定数文字列と変数を簡単に連結して、別の文字列をエレガントに作成できます。

この記事では、生成されたバイトコードを見て、この補間が内部でどのように機能するかを確認します。 また、現在の実装で可能な将来の最適化についても見ていきます。

2. 文字列補間

簡単でおなじみの例から始めましょう。

class Person(val firstName: String, val lastName: String, val age: Int) {
    override fun toString(): String {
        return "$firstName $lastName is $age years old"
    }
}

上に示したように、文字列補間を使用して toString()メソッドを実装しています。 Kotlinコンパイラがこの機能をどのように実装するかを確認するには、最初にkotlincを介してこのクラスをコンパイルする必要があります。

>> kotlinc interpolation.kt

次に、 javap ツールを使用して、生成されたバイトコードを確認できます。

>> javap -c -p com.baeldung.interpolation.Person
// truncated
public java.lang.String toString();
    Code:
       0: new           #9   // class StringBuilder
       3: dup
       4: invokespecial #13  // Method StringBuilder."<init>":()V
       7: aload_0
       8: getfield      #17  // Field firstName:LString;
      11: invokevirtual #21  // Method StringBuilder.append:(LString;)LStringBuilder;
      14: bipush        32
      16: invokevirtual #24  // Method StringBuilder.append:(C)LStringBuilder;
      19: aload_0
      20: getfield      #27  // Field lastName:LString;
      23: invokevirtual #21  // Method StringBuilder.append:(LString;)LStringBuilder;
      26: ldc           #29  // String  is
      28: invokevirtual #21  // Method StringBuilder.append:(LString;)LStringBuilder;
      31: aload_0
      32: getfield      #33  // Field age:I
      35: invokevirtual #36  // Method StringBuilder.append:(I)LStringBuilder;
      38: ldc           #38  // String  years old
      40: invokevirtual #21  // Method StringBuilder.append:(LString;)LStringBuilder;
      43: invokevirtual #40  // Method StringBuilder.toString:()LString;
      46: areturn

このバイトコード命令のセットは、StringBuilderインスタンスを作成し、append()メソッドを使用して文字列の各部分を追加します。 基本的に、このバイトコードは次のJavaコードと同等です。

new StringBuilder()
  .append(firstName)
  .append(' ') // ascii code 32 or space
  .append(lastName)
  .append(" is ")
  .append(age)
  .append(" years old")
  .toString()

したがって、文字列補間機能は、内部でStringBuilderクラスを使用しています。

2.1. 長所と短所

明るい面として、 StringBuilder の実装は非常にシンプルで、多くのJavaおよびKotlin開発者にとって理解しやすいものです。

ただし、テンプレート変数の数が増えると、バイトコードは長くなります。 処理および検証するバイトコードが増えるため、これはJVMの起動時間に影響します。

さらに、連結戦略はコンパイル時に固定されます。 したがって、新しいバージョンのコンパイラがより効率的なアプローチを使用している場合は、古いコードを再コンパイルして、その効率的な実装を利用する必要があります。

Java9とKotlin1.4.20がこれらの問題をどのように修正するかを見てみましょう。

3. ダイナミックを呼び出す

Invoke Dynamic(Indyとも呼ばれます)は、動的型付けされた言語のJVMサポートを強化することを目的とした JSR292の一部でした。 Java 9以降、 JEP 280 の一部として、Javaの文字列連結はinvokedynamicを使用しています。

この背後にある主な動機は、より動的な実装を行うことです。 つまり、バイトコードを変更せずに連結戦略を変更することが可能です。 このようにして、クライアントは、再コンパイルしなくても、新しい最適化された戦略の恩恵を受けることができます。 生成されたバイトコードも小さくなり、起動時間の短縮に貢献します。

Kotlin 1.4.20 以降、Kotlinコンパイラは文字列の連結にindyを利用できます。 そのためには、2つのことを行う必要があります。

  • indyとの文字列連結はJava9以降でのみ使用可能であるため、JVMターゲットとして9以降を使用してください。
  • -Xstring-concat コンパイラフラグを使用して、invokedynamic文字列連結を有効にします

したがって、これらのフラグを使用して同じコードを再コンパイルすると、次のようになります。

>> kotlinc -jvm-target 9 -Xstring-concat=indy-with-constants interpolation.kt

その場合、バイトコードは次のようになります。

>> javap -c -p com.baeldung.interpolation.Person
public java.lang.String toString();
    Code:
       0: aload_0
       1: getfield      #11       // Field firstName:LString;
       4: aload_0
       5: getfield      #14       // Field lastName:LString;
       8: aload_0
       9: getfield      #18       // Field age:I
      12: invokedynamic #30,  0   // InvokeDynamic #0:makeConcatWithConstants:(LString;LString;I)LString;
      17: areturn

上に示したように、バイトコードは、テンプレート変数の数に関係なく同じになります —はるかに単純で、信頼性が高く、冗長性が低くなります。

これは、Java9以降および9以上のJVMターゲットを備えたKotlin1.4.20以降でのみ機能することに注意してください。 また、Kotlin1.5ではindyをデフォルトの実装にする計画があります。

4. 結論

この簡単な記事では、Kotlinでの文字列補間の2つの異なる実装を見ました。1つは StringBuilder を使用し、もう1つはinvokedynamicを使用します。

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