1. 简介
本文将深入探讨如何通过 Java 的反射 API 访问 Kotlin 中的 companion object
。我们会先分析 companion object
在字节码层面的实现方式,然后演示如何通过反射获取并调用其中的方法。
这在跨语言混合开发(尤其是 Java 调用 Kotlin 工具类)时非常实用,稍有不慎就会踩坑 ❌,比如找不到方法或访问不到实例。
2. Kotlin 示例代码
我们先写一个典型的 Kotlin 工具类作为示例:
class XmlParsingService private constructor() {
companion object {
private var factory: DocumentBuilderFactory = DocumentBuilderFactory.newDefaultInstance()
fun extractIdFromXmlEntity(xml: String): UUID {
val document: Document = factory.newDocumentBuilder().parse(ByteArrayInputStream(xml.toByteArray()))
val node: Node = document.getElementsByTagName("entityId").item(0)
return UUID.fromString(node.textContent)
}
}
}
这个 XmlParsingService
是一个单例风格的工具类,核心逻辑封装在 companion object
中。目标是:从 Java 代码中通过反射调用 extractIdFromXmlEntity()
方法。
注意:该类构造器为 private
,且无静态方法 —— 所有公共功能都暴露在 companion object
里。
3. Companion Object 的字节码结构
Kotlin 没有 static
关键字,所以它用 companion object
来模拟静态行为。但底层是如何实现的?
✅ 关键结论:
companion object
会被编译成一个 静态内部类(static nested class),类名为EnclosingClass$Companion
。
例如上面的 XmlParsingService
编译后会生成:
- 主类:
XmlParsingService.class
- 内部类:
XmlParsingService$Companion.class
此外还需注意两点:
companion object
中定义的 方法 → 编译到$Companion
类中companion object
中定义的 字段 → 提升为外层类的静态字段(便于直接访问)
这也是为什么我们能在 companion
内直接使用 factory
字段的原因——它实际是 XmlParsingService
的一个 static
成员。
4. 通过反射访问 Companion Object
4.1 查看主类结构
使用 javap
查看 XmlParsingService.class
的结构:
public final class io.courses.baeldung.XmlParsingService
minor version: 0
major version: 55
flags: (0x0031) ACC_PUBLIC, ACC_FINAL, ACC_SUPER
this_class: #2 // io/courses/baeldung/XmlParsingService
super_class: #4 // java/lang/Object
interfaces: 0, fields: 2, methods: 2, attributes: 3
可以看到有两个 static
字段:
factory
: 对应DocumentBuilderFactory
Companion
: 对应XmlParsingService$Companion
的唯一实例(单例)
⚠️ 注意:Companion
字段名首字母大写,这是 Kotlin 编译器默认命名规则。
4.2 获取嵌套类信息
我们可以尝试通过 getClasses()
获取所有 public 内部类:
Class<XmlParsingService> xmlParsingServiceClass = XmlParsingService.class;
Class<?>[] classes = xmlParsingServiceClass.getClasses();
Assertions.assertThat(Arrays.asList(classes))
.hasSize(1)
.asList()
.first()
.isEqualTo(Class.forName("io.courses.baeldung.XmlParsingService$Companion"));
✅ 成功拿到 XmlParsingService$Companion
类引用。
4.3 反射调用 companion 方法
完整流程如下:
- 获取
Companion
静态字段 - 获取其值(即
companion
实例) - 加载
$Companion
类 - 获取目标方法并设为可访问
- 调用方法
代码实现:
Class<XmlParsingService> xmlParsingServiceClass = XmlParsingService.class;
// 步骤1:获取 Companion 字段
Field companionField = xmlParsingServiceClass.getDeclaredField("Companion");
companionField.setAccessible(true);
// 步骤2:获取 companion 实例(静态字段,传 null)
Object companionInstance = companionField.get(null);
// 步骤3:加载嵌套类
Class<?> companionClass = Class.forName("io.courses.baeldung.XmlParsingService$Companion");
// 步骤4:获取方法
Method extractIdFromXmlEntity = companionClass.getDeclaredMethod("extractIdFromXmlEntity", String.class);
extractIdFromXmlEntity.setAccessible(true);
// 步骤5:执行方法
Object result = extractIdFromXmlEntity.invoke(
companionInstance,
"<entities><entityId>8d15c2f7-635f-4730-ad60-92e2b117c4bc</entityId></entities>"
);
// 断言结果
Assertions.assertThat(result).isEqualTo(UUID.fromString("8d15c2f7-635f-4730-ad60-92e2b117c4bc"));
✅ 最终成功调用并返回正确 UUID。
💡 小贴士:如果你遇到 NoSuchFieldException
,请确认字段名是否为 Companion
(注意大小写),或者使用 getDeclaredFields()
遍历调试。
5. 总结
companion object
编译后是一个名为Outer$Companion
的静态内部类 ✅- 外部类持有一个名为
Companion
的静态字段,指向其实例 ❗ - 要通过 Java 反射调用其方法,必须先获取该实例,再在其类上查找方法
- 字段和方法的可见性需通过
setAccessible(true)
绕过检查(尤其在模块化 JVM 环境下更要注意)
这种技巧在集成测试、框架适配或遗留系统迁移中非常有用。只要理解了 Kotlin 的编译机制,Java 和 Kotlin 的边界就没那么难跨越。
示例源码详见 GitHub