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 object
或object
中的方法额外生成一个对应的 静态方法- 生成的静态方法本质是代理,内部仍调用原始的实例方法
- 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