1. 概述
本文将深入探讨 Kotlin 中的 companion object(伴生对象)机制,以及如何在 Java 代码中正确访问其成员。我们会通过简洁的示例演示核心用法,并分析 Kotlin 编译器生成的字节码,帮助你理解底层实现原理。
如果你使用 IntelliJ IDEA,可以通过 Tools → Kotlin → Show Kotlin Bytecode → Decompile 查看反编译后的 Java 代码。初次阅读时可以跳过字节码部分,重点先掌握调用方式。
✅ 核心要点:Kotlin 的
companion object
在 Java 中表现为该类的静态成员,但需要特定注解才能直接以静态方式访问。
2. 声明 companion object
在 Kotlin 中,object
关键字可用来声明线程安全的单例。当一个 object
被定义在类内部并标记为 companion
时,它就成为了该类的“伴生对象”。从 Java 的视角来看,它的成员相当于这个类的静态字段或方法。
✅ 优势:使用 companion object
后,在 Kotlin 中调用其成员时无需指定对象名,语法更简洁。
示例:
class MyClass {
companion object {
val age: Int = 22
}
}
上面的例子中我们省略了 companion object
的名称(即匿名伴生对象),此时编译器会默认使用 Companion
作为其类名。
3. 从 Java 访问 companion object 的属性
由于 Kotlin 与 Java 完全互操作,我们可以在 Java 中访问 companion object
的属性,但需注意:默认情况下这些属性不会直接暴露为 Java 的 static 字段,必须借助特定注解。
3.1 使用 @JvmField 注解暴露静态字段
若希望某个属性在 Java 中像 public static final
或 public static
那样直接访问,应使用 @JvmField
注解。
class FieldSample {
companion object {
@JvmField
val age: Int = 22
}
}
Java 中可直接访问:
public class Main {
public static void main(String[] args) {
System.out.println(FieldSample.age); // 直接访问,如同静态字段
}
}
反编译后的等效 Java 代码如下:
public final class FieldSample {
@JvmField
public static int age = 22;
@NotNull
public static final FieldSample.Companion Companion = new FieldSample.Companion((DefaultConstructorMarker)null);
public static final class Companion {
private Companion() {}
// ...
}
}
⚠️ 注意:@JvmField
使 age
成为真正的 public static
字段,绕过了 getter 方法。
3.2 特殊情况:lateinit 变量
lateinit
修饰的变量在 companion object
中会被编译为静态字段,且字段的可见性与其原始声明一致。因此,只有 public
的 lateinit
变量才能被 Java 直接访问。
class LateInitSample {
companion object {
private lateinit var password: String
lateinit var userName: String // 默认 public
fun setData(pair: Pair<String, String>) {
password = pair.first
userName = pair.second
}
}
}
Java 中只能访问 userName
:
public class Main {
static void callLateInit() {
System.out.println(LateInitSample.userName); // ✅ 允许
// System.out.println(LateInitSample.password); // ❌ 编译错误,private 不可见
}
}
反编译后可见两个静态字段生成:
public final class LateInitSample {
private static String password;
public static String userName;
@NotNull
public static final LateInitSample.Companion Companion = ...;
public static final class Companion {
@NotNull
public final String getUserName() {
String var = LateInitSample.userName;
if (var == null) Intrinsics.throwUninitializedPropertyAccessException("userName");
return var;
}
public final void setUserName(@NotNull String value) {
Intrinsics.checkNotNullParameter(value, "<set-?>");
LateInitSample.userName = value;
}
// ...
}
}
✅ 总结:
lateinit
自动生成 backing field(静态字段)- 访问仍通过 getter/setter,但 Java 可直接读写
public
字段(因 JVM 层面存在)
3.3 特殊情况:const 常量
使用 const
修饰的属性是编译期常量,效果等同于 Java 的 public static final
字段。它只能用于基本类型和 String
。
class ConstSample {
companion object {
const val VERSION: Int = 100
}
}
Java 中直接引用:
public class Main {
static void callConst() {
System.out.println(ConstSample.VERSION); // ✅ 如同 Java 静态常量
}
}
反编译结果:
public final class ConstSample {
public static final int VERSION = 100;
@NotNull
public static final ConstSample.Companion Companion = ...;
public static final class Companion {
private Companion() {}
// synthetic constructor...
}
}
✅ 关键点:
const val
→public static final
- 不需要
@JvmField
,Kotlin 自动处理 - 会被内联到调用处(inlined),提升性能
4. 从 Java 调用 companion object 的方法
要让 Java 能像调用静态方法一样调用 companion object
中的方法,必须使用 @JvmStatic
注解。
class MethodSample {
companion object {
@JvmStatic
fun increment(num: Int): Int {
return num + 1
}
}
}
Java 调用方式自然流畅:
public class Main {
public static void main(String[] args) {
int result = MethodSample.increment(1); // ✅ 直接作为静态方法调用
}
}
反编译后的字节码揭示真相:
public final class MethodSample {
@NotNull
public static final MethodSample.Companion Companion = new MethodSample.Companion(null);
@JvmStatic
public static final int increment(int num) {
return Companion.increment(num);
}
public static final class Companion {
@JvmStatic
public final int increment(int num) {
return num + 1;
}
private Companion() {}
public Companion(DefaultConstructorMarker $constructor_marker) {
this();
}
}
}
⚠️ 踩坑提醒:
@JvmStatic
会在外部类生成一个桥接的static
方法- 实际逻辑仍在
Companion
类中执行 - 若不加
@JvmStatic
,Java 必须通过MethodSample.Companion.increment(1)
调用 —— 很丑且易出错!
5. 总结
场景 | 推荐做法 | Java 是否可直接访问 |
---|---|---|
静态字段(非 const) | @JvmField |
✅ 是 |
编译期常量 | const val |
✅ 是(自动 static final) |
lateinit 变量 | public + lateinit | ✅ 是(底层有 static 字段) |
方法调用 | @JvmStatic |
✅ 是(生成 static 桥接方法) |
📌 最佳实践建议:
- 在混合项目中,凡是要被 Java 调用的
companion object
成员,务必加上对应注解 const
优于@JvmField val
,用于常量- 多利用反编译功能验证生成代码,避免黑盒踩坑
所有示例代码已托管至 GitHub:https://github.com/Baeldung/kotlin-tutorials/tree/master/core-kotlin-companion