You maintain a Kotlin library consumed heavily from Java. What are the design tradeoffs of exposing data with @JvmField instead of normal properties?
answer
- Gain: Java field ergonomics + reflection/JNI + micro-perf
- Lose: encapsulation, can't add accessor logic later
- No open/override; no visibility tightening
- Reversing it breaks Java ABI (field->method)
- Default to properties; @JvmField = deliberate escape hatch
basics
~10 s@JvmField gives Java a clean direct field and slightly cheaper access, but you lose encapsulation: you can't later add validation, laziness, or override behavior without breaking the public API for already-compiled Java callers.
solid answer
~40 sThe win: Java callers get `obj.x` ergonomics, frameworks that reflect over fields (some serializers, JNI, certain Android/game libs) work directly, and you drop trivial accessor calls. The cost is **irreversible loss of encapsulation**: a public field can never later grow a getter/setter, become computed, lazy, validated, `open`/overridable, or change visibility without breaking the ABI/source for compiled Java code. You also can't make a `@JvmField` property `private`-with-public-accessor, can't observe writes, and expose mutable state directly (thread-safety/invariants are caller's problem). Best practice: default to normal properties; reserve `@JvmField` for genuinely plain, stable data carriers or hard interop requirements (reflection/JNI/perf-critical hot fields). For constants prefer `const`; for companions weigh `const` vs `@JvmField` vs accessor. Document it as part of the public surface.
go deeper
Knows @JvmField removes accessors but may not see the encapsulation/evolution cost.
Names the encapsulation loss and a valid interop reason to use it.
Weighs ergonomics/perf vs API evolution and articulates the binary-compatibility one-way-door tradeoff.
Sets a library-wide policy: default properties, @JvmField as a documented interop boundary, with ABI and consumer-migration reasoning.
## What you gain - **Java ergonomics**: `obj.x = 5` / `obj.x` instead of `obj.setX(5)` / `obj.getX()`. - **Framework interop**: libraries that **reflect over fields** (some JSON/serialization, ORMs, JNI native access, certain Android and game engines) expect public fields, not bean accessors. - **Micro-performance**: a direct field read avoids a method call (usually negligible after JIT inlining, but real in tight loops / JNI boundaries). - **Companion/static cleanliness**: `Outer.NAME` instead of `Outer.Companion.getNAME()`. ## What you give up (the real decision driver) Exposing a **public field** is a one-way door: - **No future accessor logic**: you can never add validation, normalization, logging, lazy init, or change-tracking on read/write — there's no method to put it in. - **No polymorphism**: fields aren't virtual, so `open`/`override` is impossible; subclasses can't specialize the value. - **No visibility tightening**: you can't make it read-only-to-Java-but-writable-internally; it's just public. - **Direct mutable state**: `@JvmField var` lets any caller mutate it, so **invariants and thread-safety** become the consumer's responsibility (no setter to guard). - **ABI rigidity**: switching a published `@JvmField` back to a property changes the JVM signature (field → method), **breaking already-compiled Java callers**. The migration is a breaking change. ## A decision checklist ```text Is the data a plain, stable carrier with no invariants? -> @JvmField is fine Does a Java framework reflect over fields here? -> @JvmField (or required) Might you ever add validation/laziness/override later? -> normal property Is it a primitive/String literal constant? -> const Is it a runtime companion singleton/constant? -> @JvmField on companion ``` ## Idiomatic stance Default to **normal properties** — they preserve encapsulation and let the API evolve. Treat `@JvmField` as a deliberate interop/perf escape hatch, document it as public API, and avoid sprinkling it on mutable instance state where invariants matter. For a published library, the binary-compatibility cost of reversing the decision is the dominant factor. ```kotlin // Deliberate, documented interop carrier: class Vec3(@JvmField var x: Float, @JvmField var y: Float, @JvmField var z: Float) // vs encapsulated, evolvable: class Account(balance: Long) { var balance = balance; private set } ```
- Why is converting a published @JvmField back to a normal property a breaking change?The JVM signature changes from a field to getter/setter methods. Java code compiled against the field references a member that no longer exists, so it fails to link until recompiled — a binary-incompatible change.
- If you need both Java field-reflection AND invariants, what's a compromise?Keep the canonical state encapsulated and expose a separate plain DTO/value object with @JvmField fields at the interop boundary, or require the framework to use accessors. Don't expose your invariant-bearing state as a raw field.
@JvmField is a one-way door: easy to walk through, costly to walk back once Java callers are compiled against the field.
saying these in an interview costs you the question
- Recommending @JvmField broadly 'for performance' without measuring
- Ignoring that it permanently blocks future accessor logic
- Not recognizing it's a binary-breaking decision to reverse
- Putting @JvmField on mutable state with invariants
- Confusing it with making the property thread-safe (it doesn't)