1. 概要

この記事では、@JvmStaticアノテーションが生成されたバイトコードにどのように影響するかを見ていきます。 また、このアノテーションの使用例についても理解します。

記事全体を通して、さまざまなケースで内部で何が起こるかを確認するために、バイトコードをかなり広範囲に調べます。

2. @JvmStaticアノテーション

デモンストレーションのために、記事全体で非常に単純なKotlinファイルを使用します。 それでは、Math.ktという名前のファイルを作成しましょう。

class Math {
    companion object {
        fun abs(x: Int) = if (x < 0) -x else x
    }
}

fun main() {
    println(Math.abs(-2))
}

このファイルには、コンパニオンオブジェクト main()関数を持つクラスが含まれています。 The 主要関数はを呼び出します abs() 任意の数値の絶対値を計算する関数もちろん、これは最適な実装ではありません腹筋しかし、間違いなく良い例として役立つでしょう。

2.1. 注釈なし

私たちの多くにとって、Kotlinのコンパニオンオブジェクトは静的な動作を実装するためのツールです。 したがって、当然のことながら、Kotlinコンパイラは abs()関数を内部の静的メソッドとしてコンパイルすることを期待できます。

この仮定を検証するために、まず、Math.ktファイルをコンパイルしましょう。

>> kotlinc Math.kt

コンパイル後、3つのクラスファイルがあります。

>> ls *.class
Math$Companion.class
Math.class
MathKt.class

上記のように、1つのクラスファイルはメイン関数用、1つは Math クラス用、もう1つはコンパニオンオブジェクト用です。

次に、 javap ツールを使用して、生成されたJVMバイトコードを確認してみましょう。

>> javap -c -p Math
public final class Math {
  public static final Math$Companion Companion;

  public Math();
    Code:
       0: aload_0
       1: invokespecial #8             // Method java/lang/Object."<init>":()V
       4: return

  static {};
    Code:
       0: new           #13            // class Math$Companion
       3: dup
       4: aconst_null
       5: invokespecial #16            // Method Math$Companion."<init>":(LDefaultConstructorMarker;)V
       8: putstatic     #20            // Field Companion:LMath$Companion;
      11: return
}

>> javap -c -p -v Math
// omitted
InnerClasses:
  public static final #17= #13 of #2;     // Companion=class Math$Companion of class Math

バイトコードが表すように、Kotlinはコンパニオンオブジェクトを静的内部クラスとしてコンパイルします。 さらに、 static final fieldを定義して、内部クラスのインスタンスを囲んでいるクラス(この場合は Math クラス)内に保持します。

public static final Math$Companion Companion;

この静的フィールドを初期化するために、静的初期化ブロックを使用します。

static {};
    Code:
       0: new           #13            // class Math$Companion
       3: dup
       4: aconst_null
       5: invokespecial #16            // Method Math$Companion."<init>":(LDefaultConstructorMarker;)V
       8: putstatic     #20            // Field Companion:LMath$Companion;
      11: return

上に示したように、最初にコンパニオンオブジェクトの新しいインスタンス(インデックス0)を作成し、次にそのコンストラクター(インデックス5)を呼び出し、最後にその静的フィールド(インデックス8)内に新しいオブジェクトを格納します。

それでは、コンパニオンオブジェクトのバイトコードを確認してみましょう。

>> javap -c -p Math.Companion
public final class Math$Companion {
  // omitted
  public final int abs(int);
    Code:
       0: iload_1
       1: ifge          9
       4: iload_1
       5: ineg
       6: goto          10
       9: iload_1
      10: ireturn

}

ここでわかるように、 Kotlinはabs()関数を静的メソッドではなく単純なインスタンスメソッドとしてコンパイルします。 したがって、別のKotlin関数からこの関数を呼び出すたびに、単純なインスタンスメソッド呼び出しになります。

>> javap -c -p MathKt // main function
public final class MathKt {
  public static final void main();
    Code:
       0: getstatic     #12                 // Field Math.Companion:LMath$Companion;
       3: bipush        -2
       5: invokevirtual #18                 // Method Math$Companion.abs:(I)I
       // omitted
}

ここで、Kotlinから Math.abs()メソッドを呼び出すと、 Math.Companion.abs()への単純な(静的ではない)メソッド呼び出しに変換されることがわかります。 ] Javaからこの関数を呼び出す場合は、Math.Companion.abs()アプローチのみを使用できます。

Math.abs(-2) // won't compile
Math.Companion.abs(-2) // works

Mathクラスにabs()という名前の静的メソッドがないため、最初のメソッドはコンパイルされません。 Javaからこの機能にアクセスできる唯一の方法は、前に見たCompanion静的finalフィールドを使用することです。

2.2. 注釈付き

abs()関数に@JvmStaticアノテーションを付けると、Kotlinは追加の静的メソッドも生成します。 たとえば、これは注釈を使用する場合です。

class Math {
    companion object {
        @JvmStatic
        fun abs(x: Int) = if (x < 0) -x else x
    }
}

上記のファイルをコンパイルすると、Kotlinが abs()関数の追加の静的メソッドを実際に生成していることがわかります。

>> javap -c -p Math
public final class Math {
  public static final Math$Companion Companion;

  public static final int abs(int);
    Code:
       0: getstatic     #17            // Field Companion:LMath$Companion;
       3: iload_0
       4: invokevirtual #21            // Method Math$Companion.abs:(I)I
       7: ireturn
  // same as before
}

興味深いことに、この静的メソッドはCompanion.abs()メソッドに委任します。 この静的メソッドは、前のセクションで見た他のすべてのメソッドとクラスに加えて生成されることに注意してください。

静的メソッドが存在するため、Javaからこの関数を双方向で呼び出すことができます

Math.abs(-2)
Math.Companion.abs(-2)

ただし、 Kotlinは、静的メソッドではなくインスタンスメソッドを引き続き呼び出します。

>> javap -c -p MathKt
public final class MathKt {
  public static final void main();
    Code:
       0: getstatic     #12                 // Field Math.Companion:LMath$Companion;
       3: bipush        -2
       5: invokevirtual #18                 // Method Math$Companion.abs:(I)I
       // omitted
}

このKotlinスニペット用に生成されたバイトコードの一部を見てみましょう。

fun main() {
    println(Math.abs(-2))
}

このことから、このアノテーションはJavaの相互運用性のためにあり、純粋なKotlinコードベースには必要ないことがわかります。

さらに、 @JvmStatic アノテーションは、オブジェクトまたはコンパニオンオブジェクトのプロパティに適用することもできます。 このように、対応するgetterメソッドとsetterメソッドは、そのオブジェクトまたはコンパニオンオブジェクトを含むクラスの静的メンバーになります。

要約すると、Kotlinは、 @JvmStatic を使用すると、次のJavaコードに対応するバイトコードを生成します。

public class Math {
    public static final Companion Companion = new Companion();
    
    // only if we use @JvmStatic
    public static int abs(int x) {
        return Companion.abs(x); // referring to the static field above
    }
    
    public static class Companion {
        public int abs(int x) {
            if (x < 0) return -x;
            return x;
        }
   
        private Companion() {} // private constructor
    }
}

@JvmStatic アノテーションを省略した場合、バイトコードは静的メソッドを除いて同じになります。

3. @JvmStaticのユースケース

@JvmStaticアノテーションの最も重要なユースケースは、Javaの相互運用性です。 より具体的には、静的メソッドを使用すると、一部のJavaファーストフレームワークとの統合が容易になります。 たとえば、JUnit 5では、メソッドソースが静的である必要があります。

@ParameterizedTest
@MethodSource("sumProvider")
fun `sum should work as expected`(a: Int, b: Int, expected: Int) {
    assertThat(a + b).isEqualTo(expected)
}

companion object {
    @JvmStatic
    fun sumProvider() = Stream.of(
        Arguments.of(1, 2, 3),
        Arguments.of(5, 10, 15)
    )
}

それに加えて、 @JvmStatic メソッドの呼び出しは、Javaの世界では少し簡単で、慣用的です。

4. 結論

この記事では、@JvmStaticアノテーションが生成されたJVMバイトコードにどのように影響するかを見ました。 簡単に言うと、このアノテーションは、Kotlinコンパイラーに、内部でアノテーションが付けられた関数に対して1つの追加の静的メソッドを生成するように指示します。

さらに、このアノテーションの最も重要なユースケースは、もちろん、Javaの相互運用性の向上です。 この方法を使用すると、JUnitなどの一部のJavaフレームワークとの統合が容易になります。 また、Javaからこのようなメソッドを呼び出す方が簡単で慣用的です。

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