skip to content

Walk through the exact Java-visible members generated for a companion containing a private const val, a public const val, and a @JvmStatic var. What surprises Java callers here?

level: seniorimportance: nice to knowfreq 25%

answer

  1. private const -> invisible to Java
  2. public const -> static final, read directly, inlined
  3. const inlining -> stale value until recompile
  4. @JvmStatic var -> static getX()/setX()
  5. no @JvmStatic -> Companion.getX() hop

basics

~20 s

A private const is invisible to Java. A public const becomes a static final field read directly (Foo.X). A @JvmStatic var generates static getX()/setX() on the class. The surprise: const is inlined, so changing it can leave stale values in compiled Java.

solid answer

~40 s

Mapping each: (1) `private const val` is a compile-time constant but with private visibility — it's inlined where used inside Kotlin and is NOT part of the Java-visible API, so Java can't read it. (2) `public const val MAX = 3` becomes `public static final int MAX = 3` on the OUTER class, read as `Foo.MAX`, with the value INLINED into every compiled consumer — the classic surprise: bumping the constant in a library doesn't change already-compiled Java callers until they recompile (binary-compat hazard). (3) `@JvmStatic var counter` generates static `getCounter()`/`setCounter(int)` on `Foo` (plus the Companion-instance accessors). Surprises: the const inlining; that `private const` vanishes from the API; and that a `var` without @JvmStatic forces `Foo.Companion.getCounter()`.

code

kotlin · 12 lines
kotlin
class Foo {
    companion object {
        private const val SECRET = 42
        const val MAX = 3
        @JvmStatic var counter: Int = 0
    }
}
// Java:
// int m = Foo.MAX;            // static final, value 3 inlined
// Foo.setCounter(7);          // @JvmStatic setter
// int c = Foo.getCounter();   // @JvmStatic getter
// Foo.SECRET;                 // NOT visible (private)

go deeper

for a junior

Knows public const is read directly and a plain var needs the Companion getter.

for a middle

Lists the generated members correctly including @JvmStatic getter/setter and that private const isn't exposed.

for a senior

Explains const inlining and its binary-compatibility consequences and recommends @JvmField/@JvmStatic to avoid it.

for a principal

Treats public const changes as ABI events and sets policy for constants in shared/published Kotlin libraries consumed by Java.

## The declarations ```kotlin class Foo { companion object { private const val SECRET = 42 // private compile-time constant const val MAX = 3 // public compile-time constant @JvmStatic var counter: Int = 0 // mutable, static accessors } } ``` ## Member-by-member Java view ### `private const val SECRET` - `const` = compile-time constant; `private` limits visibility. - Inside Kotlin it's inlined at use sites; to JAVA it is effectively **invisible** (not part of the accessible API). Java cannot read `Foo.SECRET`. ### `public const val MAX` - Emitted as `public static final int MAX = 3` on the OUTER class `Foo`. - Java reads it directly: `int m = Foo.MAX;` — no getter, no `Companion`. - **Inlining surprise**: because it's a compile-time constant, the literal `3` is copied into every class that references it. If a library changes `MAX` to `5`, previously compiled Java consumers keep `3` until recompiled. This is the well-known constant binary-compatibility trap. ### `@JvmStatic var counter` - Generates static accessors on `Foo`: `public static int getCounter()` and `public static void setCounter(int)`. - The Companion-instance accessors also exist (`Foo.Companion.getCounter()`), and the generated statics delegate to the single companion instance, so state is shared. - Without `@JvmStatic`, Java would be forced to `Foo.Companion.getCounter()/setCounter()`. ## Summary of surprises 1. **`const` inlining** -> stale values in already-compiled Java until recompile. 2. **`private const`** -> not in the Java API at all. 3. **Plain `var`/`val` (no @JvmStatic, no @JvmField)** -> Java must hop through `Companion` and call getters. 4. A const value can only be a primitive or `String`; objects can't be `const`. ## Practical guidance - For published libraries, treat changing a public `const` like an ABI change. - Prefer `@JvmField`/`@JvmStatic`-backed accessors when you want runtime resolution (no inlining) and a stable contract. ## Key terms - **inlining**: copying a constant's value into the caller's bytecode at compile time. - **ABI / binary compatibility**: whether already-compiled callers keep working without recompiling. - **static accessor**: generated `getX()/setX()` static method.

  • Why is bumping a public const a binary-compatibility hazard?
    Its value is inlined into compiled consumers, so they keep the old value until recompiled — runtime and source can silently disagree.
  • How do you avoid the inlining trap but keep a simple Java field read?
    Use a non-const property with @JvmField (or @JvmStatic accessors); the value is resolved at runtime, not inlined.

saying these in an interview costs you the question

  • Saying Java can read a private const
  • Claiming const values are resolved at runtime
  • Forgetting @JvmStatic generates both getter and setter for a var
  • Thinking const works for arbitrary object types
  • Ignoring the recompile-needed staleness of const changes

context