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

此外还需注意两点:

  1. companion object 中定义的 方法 → 编译到 $Companion 类中
  2. 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 方法

完整流程如下:

  1. 获取 Companion 静态字段
  2. 获取其值(即 companion 实例)
  3. 加载 $Companion
  4. 获取目标方法并设为可访问
  5. 调用方法

代码实现:

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


原始标题:Using Java Reflection with Kotlin Companion Objects