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 finalpublic 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 中会被编译为静态字段,且字段的可见性与其原始声明一致。因此,只有 publiclateinit 变量才能被 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 valpublic 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


原始标题:Access Kotlin Companion Object in Java