1. Overview

In this article, we’re going to see how the @JvmStatic annotation affects the generated bytecode. Also, we’ll get familiar with the use cases of this annotation.

Throughout the article, we will take a peek at the bytecode pretty extensively to see what happens under the hood in different cases.

2. The @JvmStatic Annotation

For the sake of demonstration, we’re going to use a very simple Kotlin file throughout the article. So, let’s create a file named Math.kt:

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

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

This file contains a class with a companion object and a main() function. The main function calls the abs() function, which calculates the absolute value of any given number*.* This is, of course, not the best implementation for abs but will definitely serve as a good example.

2.1. Without Annotation

For many of us, the companion object in Kotlin is a tool to implement static-like behavior. So, naturally, we may expect that the Kotlin compiler will compile the abs() function as a static method under the hood.

In order to verify this assumption, first, let’s compile the Math.kt file:

>> kotlinc Math.kt

After compilation, there will be three class files:

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

As shown above, one class file is for the main function, one for the Math class, and one for the companion object.

Now, let’s take a peek at the generated JVM bytecode using the javap tool:

>> 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

As the bytecode represents, Kotlin compiles the companion object as a static inner class. Additionally, it defines a static final field to hold an instance of the inner class inside the enclosing one (in this case, the Math class):

public static final Math$Companion Companion;

In order to initialize this static field, it uses a static initializer block:

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

As shown above, it first creates a new instance of the companion object (index 0), then calls its constructor (index 5), and finally, stores the new object inside that static field (index 8).

Now, let’s check out the bytecode for the companion object:

>> 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

}

As we can see here, Kotlin compiles the abs() function as a simple instance method and not a static one. So, every time we call this function from another Kotlin function, it will be a simple instance method call:

>> 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
}

Here, we can see that calling the Math.abs() method from Kotlin translated to a simple (and not static) method call to Math.Companion.abs(). If we’re going to call this function from Java, we only can use the Math.Companion.abs() approach:

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

The first one won’t even compile because there’s no static method named abs() in the Math class. The only way we can access this functionality from Java is through the Companion static final field that we saw earlier.

2.2. With Annotation

When we put the @JvmStatic annotation on the abs() function, Kotlin will also generate an additional static method. For instance, this is when we use the annotation:

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

Now, if we compile the above file, we can see that Kotlin actually generates an addition static method for the abs() function:

>> 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
}

Interestingly, this static method delegates to the Companion.abs() method. Please note that this static method will be generated in addition to all other methods and classes we saw in the previous section.

Since there is a static method now, we can call this function from Java both ways:

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

However, Kotlin will continue to call the instance method instead of the static one:

>> 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
}

Let’s look at part of the bytecode generated for this Kotlin snippet:

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

From this, we can understand that this annotation is there for Java interoperability and won’t be necessary for pure Kotlin codebases.

Moreover, the @JvmStatic annotation can also be applied to a property of an object or a companion object. This way, the corresponding getter and setter methods will be static members in that object or the class containing the companion object.

Just to recap, Kotlin will generate a bytecode corresponding to the following Java code when we use @JvmStatic:

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
    }
}

If we omit the @JvmStatic annotation, the bytecode would be the same except for the static method.

3. Use Cases for @JvmStatic

The most important use case for the @JvmStatic annotation is Java interoperability. More specifically, with static methods, it’s easier to integrate with some Java-first frameworks. For instance, JUnit 5 requires the method sources to be static:

@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)
    )
}

In addition to that, calling @JvmStatic methods is a bit easier and more idiomatic from the Java world.

4. Conclusion

In this article, we saw how the @JvmStatic annotation affects the generated JVM bytecode. Put simply, this annotation tells the Kotlin compiler to generate one additional static method for the annotated function under the hood.

Moreover, the most important use case for this annotation is, of course, better Java interoperability. With this method, it’s easier to integrate with some Java frameworks such as JUnit. It’s also easier and more idiomatic to call such methods from Java.

As usual, all the examples are available over on GitHub.