skip to content

Which property declarations cannot be marked @JvmField, and why does each restriction exist?

level: seniorimportance: should knowfreq 35%

answer

  1. Rule: must reduce to one plain public field
  2. No custom get/set, no computed, no `by` delegate
  3. Public only — not private/protected
  4. No open/override (fields aren't virtual)
  5. lateinit var is ALLOWED; const is not

basics

~20 s

@JvmField needs a real backing field with default accessors. It's rejected on properties that are private, open/override, const, delegated, have custom get/set, are computed, or live in an interface — because there's no plain public field to expose.

solid answer

~40 s

`@JvmField` requires the property to be expressible as a single public field, so the compiler rejects it when that's impossible or contradictory: (1) **custom `get()`/`set()`** — accessors do work a raw field can't; (2) **no backing field** (computed property) — nothing to expose; (3) **delegated property `by`** — storage lives in the delegate, not a field; (4) **`private`/`protected`** — a field exposed to Java must be public; (5) **`open`/`override`** — fields can't be virtually dispatched, so polymorphism would break; (6) **`const`** — already a static final field, redundant; (7) **inside an `interface`** — interfaces hold no instance state. `lateinit var` **is** allowed, and `@JvmField` may be combined with normal `var`/`val`. The unifying rule: the property must reduce to exactly one ordinary public Java field with default access semantics.

code

kotlin · 10 lines
kotlin
class Demo {
    @JvmField var ok = 0                    // allowed
    @JvmField lateinit var name: String     // allowed
    // each line below is a compile error:
    // @JvmField val computed get() = ok     // custom getter
    // @JvmField val lazyVal by lazy { ok }  // delegated
    // @JvmField open val o = 1              // open
    // @JvmField private val p = 1           // not public
    // @JvmField const val K = 1             // const redundant
}

go deeper

for a junior

Recalls a couple of restrictions (e.g. no custom getter) even if not exhaustive.

for a middle

Lists most disallowed cases and that lateinit is allowed.

for a senior

Derives the restrictions from the 'must be one plain public field' principle and explains open/delegated/const reasoning.

for a principal

Connects the constraints to Java ABI guarantees and API-evolution implications of exposing raw fields.

## The unifying principle `@JvmField` says: *expose this property as one plain public Java field, with no method involved.* Any feature that needs a **method** or that contradicts **plain public field semantics** is therefore forbidden. Walk the list with that lens. ### Disallowed cases - **Custom accessor** (`get()`/`set()` with a body): a field can't run code on read/write. A field can't do validation or lazy computation, so the annotation is rejected. - **No backing field / computed property** (`val area get() = w * h`): there is no stored slot to expose. - **Delegated property** (`val x by lazy { ... }` or any `by`): the value is stored inside the **delegate object**, accessed via `getValue`/`setValue` operator functions — not a field. (`lazy`, `Delegates.observable`, map delegation, etc.) - **`private` / `protected` visibility**: an exposed Java field must be **public**; a non-public @JvmField would contradict its purpose, so it's disallowed. - **`open` / `override`**: Java fields are **statically dispatched** (no virtual lookup). Allowing an overridable field would silently break polymorphism, so these are rejected. - **`const`**: a `const val` already compiles to a `public static final` field; `@JvmField` would be redundant — rejected together. - **Inside an `interface`**: interfaces have no instance fields in Kotlin's model; properties there are abstract/accessor-based. - **`@JvmStatic` is not a substitute**: that's for functions/accessors, a different mechanism. ### Allowed (commonly asked) - **`lateinit var`** — *allowed*; it has a non-null backing field initialized later, perfect to expose. - Instance `val`/`var`, top-level `val`/`var`, and companion/object properties (lifting to static) with default accessors. ```kotlin class Ok { @JvmField var a = 1 // ok @JvmField lateinit var b: String // ok } class Bad { @JvmField val c get() = 1 // ERROR: custom getter, no backing field @JvmField val d by lazy { 1 } // ERROR: delegated // @JvmField private val e = 1 // ERROR: not public // @JvmField open val f = 1 // ERROR: open } ``` ### Why these matter in practice The restrictions guarantee that what Java sees is a **stable, simple field** with predictable read/write semantics — no hidden method calls, no broken overriding, no surprising inlining. If you need any of those (encapsulation, laziness, polymorphism), `@JvmField` is the wrong tool and you keep the normal property.

  • Why is @JvmField allowed on `lateinit var` but not on a `lazy` delegated property?
    `lateinit var` has a real backing field (just initialized later), which can be exposed. A `lazy` property stores its value in a delegate object accessed via getValue, so there's no plain field to expose.
  • Why can't an `open` property be @JvmField?
    Java field access is statically bound — there's no virtual dispatch on fields — so exposing an overridable property as a field would break polymorphic behavior.

saying these in an interview costs you the question

  • Saying @JvmField works on computed/delegated properties
  • Claiming lateinit var is disallowed (it's allowed)
  • Thinking a private property can be @JvmField
  • Not knowing open/override is rejected and why
  • Treating @JvmField and const as combinable

context