1. 概述

本文将深入探讨 Kotlin 中 @JvmStatic 注解对生成字节码的影响,并分析其典型使用场景。

我们会多次查看编译后的 JVM 字节码,以直观理解不同情况下底层发生了什么。这种“看 bytecode 下饭”的方式虽然有点硬核,但能帮你真正搞懂原理,避免日后踩坑 ❌。

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

该文件包含一个带有 companion object 的类和一个 main() 函数。main 函数调用了 abs() 方法来计算绝对值(当然这不是最优实现,但足够说明问题)。

2.1. 不加 @JvmStatic 的情况

✅ 很多开发者习惯性地把 Kotlin 的 companion object 当作“静态工具类”来用。于是自然会认为 abs() 方法会被编译成 JVM 上的静态方法。

但事实并非如此。我们先编译这个文件:

>> kotlinc Math.kt

编译后生成三个 class 文件:

>> ls *.class
Math$Companion.class
Math.class
MathKt.class
  • Math.class:主类
  • Math$Companion.class:companion object 编译后的静态内部类
  • MathKt.class:顶层函数 main() 编译后的类

使用 javap 工具查看 Math 类的字节码:

>> 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 将 companion object 编译为一个 静态内部类Math$Companion
  • 在外层类 Math 中定义了一个 public static final 字段 Companion,持有该内部类的唯一实例
  • 静态代码块负责初始化这个字段(new → 构造 → 存入 static field)

再看 Math$Companion 的字节码:

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

⚠️ 注意: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
}

字节码清晰显示:先获取 Companion 实例,再通过 invokevirtual 调用其实例方法。

如果你尝试从 Java 调用:

Math.abs(-2); // ❌ 编译失败!没有这个静态方法
Math.Companion.abs(-2); // ✅ 正确调用方式

所以,不加 @JvmStatic 时,Java 只能通过 .Companion 访问,非常别扭。

2.2. 添加 @JvmStatic 后的变化

当我们给方法加上 @JvmStatic

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

重新编译后,查看 Math 类的字节码:

>> 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
  // 其余内容同前
}

✅ 关键变化:

  • Kotlin 额外生成了一个 静态方法 abs(int)
  • 这个静态方法内部会委托调用 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
}

⚠️ 这说明 @JvmStatic 完全是为 Java 互操作性 设计的,在纯 Kotlin 项目中基本无用武之地。

此外,@JvmStatic 也可用于对象或 companion object 中的属性,使其 getter/setter 成为静态方法。

总结一下,加上 @JvmStatic 后,等效的 Java 代码大致如下:

public class Math {
    public static final Companion Companion = new Companion();
    
    // 因为使用了 @JvmStatic 才生成
    public static int abs(int x) {
        return Companion.abs(x);
    }
    
    public static class Companion {
        public int abs(int x) {
            if (x < 0) return -x;
            return x;
        }
   
        private Companion() {}
    }
}

如果不加 @JvmStatic,就只是少了那个额外的静态 abs 方法。

3. @JvmStatic 的典型使用场景

@JvmStatic 最核心的价值在于提升与 Java 生态的兼容性。以下是几个常见场景:

3.1. JUnit 5 参数化测试

JUnit 5 的 @MethodSource 要求提供数据的方法必须是 静态的。此时 @JvmStatic 就必不可少了:

@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,JUnit 无法找到 sumProvider 这个静态方法,测试会直接报错。

3.2. 与 Java 框架集成

许多 Java 框架(如 Spring、Jackson、反射工具类)在扫描方法时默认查找静态成员。例如:

  • 工具类暴露公共静态 API
  • 序列化框架通过反射调用静态工厂方法
  • AOP 拦截静态方法调用点

在这种混合技术栈项目中,@JvmStatic 能显著降低集成成本。

3.3. 提升 Java 调用体验

即使不是强制要求,从 Java 调用 Math.abs() 也比 Math.Companion.abs() 更符合直觉和编码习惯,代码更简洁、更“地道”。

4. 总结

  • @JvmStatic 的作用是让 Kotlin 编译器为 companion objectobject 中的方法额外生成一个对应的 静态方法
  • 生成的静态方法本质是代理,内部仍调用原始的实例方法
  • Kotlin 代码调用不受影响,依然走实例方法;只有 Java 能享受到静态调用的便利
  • 主要用途是 Java 互操作性,尤其适用于:
    • JUnit 等要求静态方法的测试框架
    • 需要静态入口的 Java 框架集成
    • 提升 Java 侧调用的可读性和简洁性

📌 简单说:你在写 Kotlin 库并希望被 Java 项目友好调用时,@JvmStatic 是必备技能;如果是纯 Kotlin 项目,则几乎用不到。

所有示例代码均可在 GitHub 获取:https://github.com/Baeldung/kotlin-tutorials/tree/master/core-kotlin-modules/core-kotlin-annotations


原始标题:The @JvmStatic Annotation in Kotlin