skip to content

When a Java developer calls into a Kotlin class that has a companion object, what is the 'Companion' field they see in autocomplete, and how should they reach the companion's members from Java?

level: juniorimportance: must knowfreq 58%

answer

  1. Companion -> nested class + static field
  2. Java: MyClass.Companion.foo()
  3. @JvmStatic skips .Companion
  4. @JvmField / const -> real static field
  5. Kotlin never writes .Companion

basics

~10 s

Kotlin turns a companion object into a static field named Companion plus a nested class. From Java you call MyClass.Companion.method(), unless the member is annotated @JvmStatic, which lets you call MyClass.method() directly.

solid answer

~40 s

A Kotlin companion object is compiled to a nested static class named Companion and a public static final field MyClass.Companion holding its singleton instance. Java sees both, which clutters completion. To invoke a companion function from Java you write MyClass.Companion.foo(); plain Companion members are NOT static methods on the outer class. Annotating the companion member with @JvmStatic also emits a true static bridge so Java can call MyClass.foo() directly. A companion val with @JvmField (or const for primitives/String) exposes a real static field; otherwise Java must use the generated getter MyClass.Companion.getX(). So the Companion field is the bridge Kotlin generates to give Java a handle on the companion singleton.

code

kotlin · 11 lines
kotlin
class Config {
    companion object {
        const val VERSION = 3            // Config.VERSION (static field)
        @JvmStatic fun load() = Config()  // Config.load() from Java
        fun parse(s: String) = Config()   // Config.Companion.parse(s) from Java
    }
}
// Java:
//   int v = Config.VERSION;
//   Config c = Config.load();
//   Config d = Config.Companion.parse("x");

go deeper

for a junior

Knows companion -> Companion field and that Java calls MyClass.Companion.foo().

for a middle

Distinguishes @JvmStatic (static bridge) from @JvmField/const (static field) and getter access.

for a senior

Explains the generated nested class + field, reflection clutter, and how serializers/DI must skip it.

for a principal

Weighs API design: when to add @JvmStatic/@JvmField for clean Java ergonomics vs. leaving Kotlin-idiomatic; understands tooling implications across the codebase.

## What a companion object compiles to In Kotlin a `companion object` holds members shared by all instances of a class (the rough equivalent of Java `static`). But the JVM has no notion of a companion, so the compiler generates **synthetic artifacts** to bridge it: - A **nested class** named `MyClass$Companion` containing the companion's methods/properties as instance members. - A **public static final field** `MyClass.Companion` holding the single instance of that nested class. This is the synthetic `$`-adjacent member that shows up in Java completion and reflection and surprises people. ```kotlin class Repo { companion object { fun create(): Repo = Repo() @JvmStatic fun createStatic(): Repo = Repo() const val TAG = "repo" // real static field @JvmField val SHARED = listOf(1) // real static field val cached = mutableListOf<Int>() // getter only } } ``` ## Calling it from Java ```java Repo.Companion.create(); // plain companion fun Repo.createStatic(); // @JvmStatic emits a static bridge String t = Repo.TAG; // const -> static field List<Integer> s = Repo.SHARED; // @JvmField -> static field Repo.Companion.getCached(); // plain val -> getter on Companion ``` ## The annotations that change the picture - **`@JvmStatic`** on a companion function/property generates an additional real `static` method on the **enclosing** class, so Java skips `.Companion`. - **`@JvmField`** on a companion `val`/`var` exposes a real static field (no getter). - **`const`** (compile-time constant of a primitive or `String`) always becomes a static field. From **Kotlin** you never write `.Companion`; you write `Repo.create()` directly — the compiler resolves it. The `Companion` field only matters at the Java boundary and in reflection, where it pollutes completion. ## Why it matters Reflection over `Repo::class.java` will list a `Companion` field and a `Companion` nested class. Tooling that scans fields (serializers, DI) may need to skip it.

  • Why might a JSON serializer accidentally try to serialize the Companion field?
    Reflection-based serializers iterate declared fields; the synthetic public static final Companion field appears and must be filtered (most respect transient/synthetic flags or are configured to skip it).
  • Does @JvmStatic remove the Companion field?
    No. It adds a static bridge method on the enclosing class but the Companion nested class and static field still exist.

The Companion field is like a receptionist desk: Java must walk up to MyClass.Companion before it can talk to any companion member, unless @JvmStatic gives that member a direct phone line.

saying these in an interview costs you the question

  • Claiming plain companion members are static on the outer class
  • Saying Kotlin code must also write .Companion
  • Thinking @JvmStatic deletes the Companion field
  • Confusing @JvmField with @JvmStatic
  • Assuming every companion val becomes a static field

context