skip to content

Explain @JvmStatic on companion members: what it generates, its interaction with @JvmField/const, and the interop trade-offs.

level: seniorimportance: should knowfreq 40%

answer

  1. @JvmStatic = extra static bridge on the outer class
  2. Keeps the companion instance member too
  3. Property -> static getter/setter; @JvmField -> static field
  4. const val is already static; @JvmStatic redundant/disallowed
  5. Removing @JvmStatic breaks Java binary callers

basics

~10 s

@JvmStatic tells the compiler to also emit a real static method or accessor on the outer class, so Java can call MyClass.foo() instead of MyClass.Companion.foo(). The companion still exists underneath.

solid answer

~40 s

On a companion function or property, @JvmStatic generates a true JVM static member on the enclosing class that forwards to the companion's instance member; the companion instance method/getter is kept, so both call forms work from Java. For properties, @JvmStatic produces static getter/setter; for plain fields you instead use @JvmField (no accessors) or const val (a static final inlined constant — already static, so @JvmStatic is redundant and disallowed on const). @JvmStatic only affects JVM/Java-facing output; Kotlin call sites are unchanged. Trade-offs: it improves Java ergonomics and avoids the Companion indirection, but adds extra synthesized members (slightly larger bytecode) and means removing it later is a source-compatible but Java-binary-affecting change. It cannot be applied to companions of interfaces' members the same way, and only valid inside objects/companion objects.

code

kotlin · 11 lines
kotlin
class Mathx {
    companion object {
        @JvmStatic fun square(x: Int) = x * x
        const val E = 2.718281828
    }
}

// Java:
//   Mathx.square(3);          // works because of @JvmStatic
//   double e = Mathx.E;       // const val -> static final
//   Mathx.Companion.square(3) // also still works

go deeper

for a junior

Knows @JvmStatic helps Java call companion members without 'Companion'.

for a middle

Explains it generates a static bridge while keeping the companion member.

for a senior

Distinguishes function/property/field cases, const redundancy, and interop trade-offs.

for a principal

Treats @JvmStatic as binary contract; chooses companion vs top-level vs const for public API stability.

## What @JvmStatic does Placed on a member of a **companion object** (or any `object`), `@JvmStatic` directs the Kotlin compiler to emit an **additional real static** member on the **enclosing class** that delegates to the companion's instance member. ```kotlin class Clock { companion object { @JvmStatic fun now(): Long = System.currentTimeMillis() } } ``` Generated (conceptually): - `Clock$Companion.now()` — the instance method (unchanged), - `Clock.now()` — a static method that calls `Companion.now()`. So from **Java** both `Clock.now()` and `Clock.Companion.now()` work; without `@JvmStatic`, only the latter does. **Kotlin call sites are identical either way** (`Clock.now()`). ## Functions vs properties vs fields - **Function:** generates a static method bridge. - **Property:** generates static **getter** (and setter if `var`). The backing field stays in the companion unless you also add `@JvmField`. - **`@JvmField val x`:** exposes a **public static field** with no accessors — use when Java should read the field directly. You do not combine it with `@JvmStatic` (it already targets the field as static). - **`const val`:** a compile-time constant of a primitive or `String`; it is already `public static final` and **inlined** at call sites. `@JvmStatic` is **redundant/not allowed** on `const`. ```kotlin class Limits { companion object { const val MAX = 100 // static final, inlined @JvmField val defaults = intArrayOf(1, 2) // public static field @JvmStatic var verbose = false // static getter+setter for Java } } ``` ## Interop and design trade-offs - **Pro:** cleaner Java API (`MyClass.foo()`), no `Companion` noise, natural for libraries with mixed Kotlin/Java consumers. - **Con:** extra synthesized members (marginal bytecode/size); two ways to reach the same logic; the forwarding indirection (negligible, often inlined by JIT). - **Compatibility:** adding `@JvmStatic` is backward compatible; **removing** it breaks Java callers that used the static form. For published libraries, treat it as part of the binary contract. - **Scope:** only valid in `object`/`companion object` members. An interface companion can use it on the JVM (target 1.8+) for default-like statics, but plain abstract members cannot. ## Decision guide Use `@JvmStatic` when Java/Kotlin-mixed consumers call the member often; use `const`/`@JvmField` for constants and exposed fields; if the API is Kotlin-only, skip all of these and just rely on the companion. If no class association is needed, prefer **top-level** declarations, which compile to statics on a `*Kt` file class.

  • Can you put @JvmStatic on a 'const val'?
    No. const val is already a static final field, so @JvmStatic is redundant and not allowed there.
  • After adding @JvmStatic, does MyClass.Companion.foo() still work from Java?
    Yes. The companion instance member remains; @JvmStatic only adds a static forwarder, it does not remove anything.
  • If a helper needs no class association, what is often simpler than a companion?
    A top-level function/property, which compiles to a static on the file's generated *Kt class.

saying these in an interview costs you the question

  • Saying @JvmStatic moves the member out of the companion (it adds, not moves)
  • Combining @JvmStatic with const val
  • Claiming @JvmStatic changes Kotlin call syntax
  • Ignoring that removing @JvmStatic is a Java binary-breaking change
  • Confusing static getter generation with @JvmField direct field exposure

context