skip to content

How is a `lateinit var` exposed to Java, and what is the 'synthetic guard' associated with it?

level: seniorimportance: should knowfreq 35%

answer

  1. lateinit -> public non-null field, direct Java access
  2. Synthetic guard: read before init throws UninitializedPropertyAccessException
  3. ::x.isInitialized checks the sentinel without throwing
  4. var only, non-null non-primitive type
  5. Java field read bypasses the Kotlin guard -> can see null

basics

~10 s

A lateinit var becomes a real public field that Java can read and write directly. Kotlin still inserts a hidden check so that reading it before assignment throws an exception instead of returning null.

solid answer

~50 s

`lateinit var name: T` exposes the backing field with the property's visibility (a public lateinit -> a **public, non-null field**), so Java accesses it directly as `obj.name`. Unlike an ordinary property, lateinit emits **no accessor logic on the field write**, but the *Kotlin* read site is wrapped in a **synthetic guard**: reading the property before it is assigned throws `UninitializedPropertyAccessException` rather than yielding null. The compiler uses a sentinel (uninitialized null) to detect this. The matching `::name.isInitialized` check compiles to reading the field and comparing against that sentinel. Caveats: lateinit only works on non-null, non-primitive `var`s, cannot have custom accessors, and from Java the guard is partly bypassable — Java can read the raw field and see `null` because Java does not run the Kotlin read-guard, so direct Java field reads are not protected the way Kotlin reads are.

code

kotlin · 6 lines
kotlin
class Service {
    lateinit var conn: Connection   // public field; Kotlin read inserts a null-check guard
    fun isReady() = ::conn.isInitialized
}
// Kotlin read of conn before assignment -> UninitializedPropertyAccessException
// Java reading the field directly may observe null (guard bypassed)

go deeper

for a junior

Knows lateinit defers initialization and throws if read too early.

for a middle

Knows it exposes a public field and that ::x.isInitialized exists; states var/non-null/non-primitive constraints.

for a senior

Explains the synthetic guard mechanism (sentinel + injected check) and the Java-bypass interop gotcha.

for a principal

Reasons about exposing lateinit fields across the Java boundary as a safety/ABI risk and prescribes alternatives (constructor injection, nullable + check).

## What `lateinit` is for `lateinit var` lets you declare a **non-null** `var` without initializing it at construction time — you promise to assign it before first read. It is common for dependency injection, test setup (`@BeforeEach`), and framework-managed fields. ## JVM exposure The property is backed by a **field with the property's visibility**. A `public lateinit var service: Service` becomes a **public field** that Java sees and can read/write directly: ```java obj.service = new Service(); // direct field write Service s = obj.service; // direct field read ``` There is no separate uninitialized-null wrapper type — the field's static type is the non-null type, but its actual stored value starts as `null` until assigned. ## The synthetic guard The safety comes from how Kotlin compiles **reads** of a lateinit property. Instead of returning the field value blindly, the compiler inserts a check: ```kotlin // conceptual expansion of `val x = service` val tmp = service$field if (tmp == null) throw UninitializedPropertyAccessException("lateinit property service has not been initialized") val x = tmp ``` That injected null-check-and-throw is the **synthetic guard**. It turns a premature read into a clear exception rather than a silent null or NPE deep in your code. ## `isInitialized` Kotlin exposes `::name.isInitialized` (only on the dispatch receiver in scope) which compiles to reading the backing field and testing it against the uninitialized sentinel — a way to check without triggering the throw. ```kotlin class Repo { lateinit var db: Database fun ready() = ::db.isInitialized } ``` ## Constraints - Only on `var` (not `val`). - The type must be **non-null** and **non-primitive** (`Int`, `Boolean`, etc. are not allowed — use a nullable or a default instead). - **No custom getter/setter** and **no delegate**. ## Interop gotcha The guard protects **Kotlin reads**. When **Java** reads the exposed public field directly, it bypasses the Kotlin read path and can observe the raw `null`, then hit a normal `NullPointerException` later — Java is not protected by the synthetic guard. This is why exposing lateinit fields across the Java boundary needs care. ## Why it is not `@JvmField`-compatible lateinit already exposes a field with the visibility you declared, so combining it with `@JvmField` is redundant/disallowed.

  • Why can't a lateinit property be `Int`?
    Primitives have no null sentinel to mark 'uninitialized', so the guard mechanism cannot distinguish unset from zero; lateinit requires a non-null reference type.
  • Does the synthetic guard protect Java callers reading the field directly?
    No. The guard lives in the Kotlin read path; a direct Java field read bypasses it and may observe the raw null.

saying these in an interview costs you the question

  • Saying lateinit exposes a getter/setter pair like a normal var
  • Claiming lateinit works on primitives or nullable types
  • Thinking reading before init returns null instead of throwing in Kotlin
  • Assuming the guard also protects direct Java field reads
  • Confusing lateinit with by lazy

context