skip to content

How do val and var compile on the JVM, and what does that mean for Java interop and the meaning of 'final'?

level: seniorimportance: nice to knowfreq 38%

answer

  1. val property -> getX(); var -> getX()+setX()
  2. local val -> JVM final local
  3. backing field is private; @JvmField exposes it
  4. @JvmField val -> real Java public final field
  5. const val -> inlined static final compile-time constant

basics

~20 s

A val property becomes a Java getter method, and a var adds a setter. A local val compiles to a final local variable. val controls reassignment in Kotlin, but Java code calling the getter just sees a normal method, so val is not a deep immutability promise across the boundary.

solid answer

~50 s

On the JVM, a Kotlin **property** compiles to a private backing field plus accessor methods: a `val` emits `getX()`; a `var` emits `getX()` and `setX()`. The backing field itself is `private` and typically `final` for a `val`. A **local** `val` compiles to a `final` local variable. So `val` maps to JVM `final`-ish semantics for reassignment, but Java callers interact through the generated getters/setters, not the field. Consequences: a Kotlin `val` exposed to Java is read-only only because no setter exists — Java can't reassign it, but reflection could still touch the field. `@JvmField` suppresses accessor generation and exposes the backing field directly (then a `val` field is a real Java `final` field). For top-level/`object` constants, `const val` becomes a true JVM compile-time constant inlined at call sites. None of this changes that `val` constrains the reference, not the referenced object's mutability.

code

kotlin · 5 lines
kotlin
class Settings(
    val mode: String,            // Java: getMode()
    var retries: Int,            // Java: getRetries() + setRetries()
    @JvmField val tag: String    // Java: settings.tag (public final field)
)

go deeper

for a junior

Knows val and var become getters/setters when seen from Java.

for a middle

Explains that val omits the setter and that local vals are final.

for a senior

Details backing fields, @JvmField, and how const val differs, reasoning about interop surface.

for a principal

Weighs accessor vs field exposure, reflection escape hatches, and immutability guarantees when designing a cross-language API.

## Properties become accessor methods Kotlin doesn't expose fields directly by default. A property compiles to: - a **private backing field** (when one is needed), plus - **accessor methods**: `getX()` for `val`, and `getX()` + `setX(...)` for `var`. ```kotlin class User(val id: Long, var name: String) ``` From Java this looks like: ```java user.getId(); // val -> getter only user.getName(); user.setName("Bob"); // var -> setter exists // no user.setId(...) // val -> no setter generated ``` So from Java, a `val` is read-only purely because **no setter is generated** — there's no magic `final` enforcement on the call site beyond that. ## Local vals are JVM `final` A local `val` compiles to a `final` local variable in bytecode. This is what lets it be safely captured by lambdas/closures without the "effectively final" gymnastics Java sometimes needs. ## The backing field and `final` The generated **backing field** for a `val` is `private` and generally `final`; for a `var` it's `private` and non-final. But because access goes through methods, Java code can't even see the field normally. ## `@JvmField` — expose the field directly `@JvmField` tells the compiler to skip accessor generation and expose the backing field as a public Java field. A `@JvmField val` becomes a genuine Java `public final` field: ```kotlin class Point(@JvmField val x: Int, @JvmField val y: Int) // Java: point.x (a real final field, no getX()) ``` ## `const val` — true compile-time constants For `val` declarations at the top level or in an `object`/`companion object` with primitive or `String` type, `const val` produces a **compile-time constant** that is *inlined* into call sites and becomes a JVM `static final` constant: ```kotlin object Config { const val VERSION = "2.1" } // usages of Config.VERSION are inlined; truly constant ``` (That's a sibling leaf — mentioned here only to contrast `val` vs `const val`.) ## Interop takeaways - Java can reassign a Kotlin `var` (setter exists) but not a `val` (no setter). - `val` is **not** a deep-immutability contract across the boundary — the returned object can still be mutable. - Reflection can bypass accessor-based read-only semantics. - Use `@JvmField` when you want a plain Java field; use `const val` for inlined constants. ## Still: reference, not object All of this is about the *binding/field*. A `val` returning a `MutableList` is just as mutable from Java as from Kotlin.

  • Can Java reassign a Kotlin val property?
    No — the compiler generates no setter, so there's no setX() to call. (Reflection on the backing field is a separate escape hatch.)
  • What does @JvmField change for a val?
    It suppresses the getter and exposes the backing field directly, producing a genuine Java public final field instead of an accessor method.

saying these in an interview costs you the question

  • Saying val properties are public fields by default (they're accessor-based)
  • Claiming Java can never touch a val (reflection can)
  • Confusing const val with @JvmField
  • Thinking val gives deep immutability across the Java boundary
  • Not knowing a var generates a setter visible to Java

context