You own a Kotlin library heavily consumed by Java. How would you decide a consistent policy for @JvmStatic (and the related interop annotations) across factory/entry-point APIs?
answer
- Interop annotations = public ABI → write a policy
- Entry points/factories → @JvmStatic
- Pair with @JvmOverloads/@Throws/@JvmName/@JvmField/const
- Removing @JvmStatic breaks Java callers (ABI)
- Enforce with binary-compatibility-validator + Java tests
basics
~10 sDecide deliberately, not case by case. Apply @JvmStatic to all public companion/object entry points so Java gets clean Foo.create() calls, document it, and combine with @JvmName/@Throws/@JvmOverloads for an idiomatic Java surface.
solid answer
~40 sTreat interop annotations as part of the public ABI, governed by an explicit policy rather than sprinkled ad hoc. For any companion/object member that is a public factory or entry point, apply `@JvmStatic` so Java sees `Foo.create()` instead of `Foo.Companion.create()`. Pair it with siblings: `@JvmOverloads` to expand default-argument functions into Java-callable overloads, `@Throws` so checked exceptions appear in Java signatures, `@JvmName` to fix mangled or clashing names, and `@JvmField`/`const` for value exposure. Be aware these decisions are **binary-compatibility sensitive**: removing `@JvmStatic` later drops the static method and breaks Java callers; switching a constant between `const` and a `@JvmStatic` accessor changes call shape. Enforce the policy with API-compatibility tooling (e.g., binary-compatibility-validator) and lint/conventions. Consider whether top-level functions would be simpler than a companion at all, since they're already static.
code
kotlin · 8 linesclass Parser private constructor() {
companion object {
@JvmStatic @JvmOverloads @Throws(ParseException::class)
fun parse(input: String, strict: Boolean = false): Result =
// Java: Parser.parse("x"); Parser.parse("x", true);
Result(input, strict)
}
}go deeper
Can state @JvmStatic helps Java callers but won't frame a library-wide policy.
Suggests applying it to public factories and pairing with @JvmOverloads/@Throws.
Designs a coherent policy across the interop annotation family with consistency in mind.
Governs it as ABI: binary-compatibility consequences, validator tooling, Java consumption tests, and build-time enforcement.
## Frame it as ABI governance, not a per-function whim For a library with a large Java audience, the set of generated JVM members **is the public ABI**. Interop annotations decide that ABI, so they deserve a written policy. ## A workable default policy 1. **Entry points / factories in companions and objects → `@JvmStatic`.** Gives Java `Foo.create(...)`, the idiomatic factory shape, and hides the `Companion` artifact. 2. **Public functions with default arguments → `@JvmOverloads`** so Java (which has no default parameters) gets the expected overload set. 3. **Functions that can fail in ways Java should catch → `@Throws`** to surface checked exceptions in the Java signature. 4. **Name clashes / property-mangling / reserved words → `@JvmName`** (and `@file:JvmName` to name file classes for top-level functions). 5. **Value exposure**: true compile-time constants → `const val`; plain field access with no logic → `@JvmField`; values needing accessors → `@JvmStatic` property. 6. **Prefer top-level functions** when there's no need to attach to a type — they're already static, so no annotation is needed and the surface is simpler. ## Binary-compatibility consequences - **Removing `@JvmStatic`** deletes the outer static method → existing compiled Java callers that used `Foo.bar()` break with `NoSuchMethodError`. - **`const` ⇄ accessor swaps** change call shape (inlined field vs `getX()`), and `const` values are baked into already-compiled callers, so changing the value requires their recompilation. - **`@JvmName` renames** are also breaking once published. ## Enforcement - Use **binary-compatibility-validator** (the `.api` dump) to make ABI changes show up in code review. - Add **convention/lint rules** so contributors apply the policy uniformly. - Maintain Java consumption tests so the Java-facing call shapes are exercised, not just Kotlin. ## When NOT to use `@JvmStatic` - Internal-only members with no Java consumers — it just adds noise/bytecode. - When the design should really be a top-level function or an `object` used as a singleton instance. ```kotlin class HttpClient private constructor(/* ... */) { companion object { @JvmStatic @JvmOverloads @Throws(IOException::class) fun create(timeoutMs: Int = 30_000): HttpClient = HttpClient(/* ... */) // Java: HttpClient.create(); HttpClient.create(5000); } } ```
- Why is removing @JvmStatic a binary-incompatible change?It deletes the generated outer static method; previously compiled Java code calling Foo.bar() now fails at runtime with NoSuchMethodError.
- How would you catch accidental ABI changes from these annotations in CI?Run binary-compatibility-validator to dump and diff the public .api; any change to generated members surfaces in review and must be intentional.
saying these in an interview costs you the question
- Treating @JvmStatic as a cosmetic per-function choice with no ABI impact
- Ignoring that removing it breaks compiled Java callers
- Forgetting @JvmOverloads for default-argument functions
- No enforcement/tests for the Java-facing surface
- Overusing it on internal members with no Java consumers