skip to content

Where is @JvmStatic NOT allowed, and what subtle issue arises with @JvmStatic on a companion inside an interface?

level: seniorimportance: should knowfreq 30%

answer

  1. Only on companion/named-object members
  2. Rejected: top-level, normal class member, const val
  3. Interface companion needs JVM target 8+ (bytecode 52)
  4. Static forwarder = no dynamic dispatch
  5. Pairs with @JvmName/@Throws, not @JvmField

basics

~10 s

You can only use @JvmStatic on companion or named-object members. It is rejected on top-level functions, regular class members, and const vals. On interface companions it works but historically needed a newer JVM target.

solid answer

~50 s

`@JvmStatic` is valid only on members of a **companion object** or a **named `object`**. It is a compile error on: top-level functions/properties (already compiled to static members of a file class), members of a regular (non-object) class, `const val` (already a static final field), and on the companion object declaration itself rather than its members. The interesting edge case is a companion **inside an interface**: emitting a real static method on an interface requires `static` methods in interfaces, which the JVM only supports from bytecode version 52 (Java 8). With modern Kotlin defaulting to Java 8+ targets this works fine, and the static forwarder is placed on the interface. You should also know that the forwarder is not `open`/virtual — `@JvmStatic` members cannot be overridden polymorphically through the static entry point, since static methods are not subject to dynamic dispatch.

go deeper

for a junior

May only know it works on companion objects, without the precise rejection list.

for a middle

Lists the main invalid placements (top-level, const val, normal class).

for a senior

Adds the interface-companion JVM-target nuance and the no-dynamic-dispatch property of the static forwarder.

for a principal

Turns the rules into a consistent interop API policy and reasons about cross-target/bytecode-version compatibility.

## Where `@JvmStatic` is rejected The annotation is **only** meaningful and allowed on members declared inside a `companion object` or a named `object`. The compiler reports an error for: - **Top-level functions/properties** — these already compile to `static` members of a synthetic file class (e.g. `UtilKt`), so a static-making annotation is meaningless. (Renaming that file class is `@file:JvmName`, a different concern.) - **Members of a normal class** — instance members can't be static. - **`const val`** — already a `static final` field with an inlined constant; nothing to convert. - **The companion/object declaration itself** — you annotate the *members*, not the object. ## Interface companion edge case ```kotlin interface Codec { companion object { @JvmStatic fun default(): Codec = Utf8Codec } } ``` Generating `Codec.default()` requires a **static method on an interface**, which is only legal in JVM bytecode **version 52 (Java 8) and later**. With older `-jvm-target 1.6`, this was disallowed. Modern Kotlin defaults to Java 8+ (commonly 17/21), so the static forwarder is emitted directly on the interface and Java can call `Codec.default()`. ## No virtual dispatch A `@JvmStatic` forwarder is a **static** method. Static methods are resolved at compile time on the class named, not via the receiver's runtime type — there is **no dynamic dispatch**. So even if a subclass had its own companion member of the same name, calling through the static entry point does not pick it up polymorphically. The companion's *instance* method can be `open`/overridable within the companion hierarchy, but the static shortcut is fixed to the class where it's declared. ## Combination rules - Cannot combine with `@JvmField` (methods vs field). - Redundant/disallowed on `const val`. - Compatible with `@JvmName` (you can rename the generated static method) and with `@Throws` (declare checked exceptions on it). ## Why it matters in design When authoring a Kotlin library consumed by Java, scattering `@JvmStatic` ad hoc creates an inconsistent surface. A deliberate policy — e.g., apply it to all public factory/entry-point functions in companions/objects — gives Java users an idiomatic, `Foo.create()`-style API.

  • Why does an interface companion's @JvmStatic depend on the JVM target?
    Static methods in interfaces are only legal from bytecode 52 (Java 8); older targets like 1.6 couldn't host the generated static forwarder on the interface.
  • If a subclass redeclares a same-named @JvmStatic member, does Java dispatch polymorphically through the static call?
    No. Static methods bind to the named class at compile time; there is no runtime dispatch, so the static entry point is not polymorphic.

saying these in an interview costs you the question

  • Claiming @JvmStatic works on top-level functions
  • Saying it can be applied to ordinary class methods
  • Believing the static forwarder participates in virtual dispatch
  • Not knowing the interface/JVM-target constraint
  • Trying to combine it with @JvmField on the same member

context