skip to content

What problem does Delegates.notNull() solve, and how does it behave if you read it before assigning a value?

level: middleimportance: should knowfreq 38%

answer

  1. notNull() = late-set non-null var without initializer
  2. Fills the gap lateinit can't: primitives
  3. Read-before-set throws IllegalStateException
  4. Reassignable after first set
  5. lateinit preferred for references (cheaper, .isInitialized)

basics

~10 s

Delegates.notNull() lets you declare a non-null var without giving it a value right away. You assign it later. If you read it before setting it, you get an exception instead of a silent null.

solid answer

~40 s

Delegates.notNull<T>() returns a delegate for a non-null `var` that has no initial value — useful when the value is known only after construction (e.g. injection, lifecycle callbacks). It is an alternative to `lateinit var` for cases where `lateinit` cannot be used: notably **primitive types** (`Int`, `Long`, `Boolean`, etc.), which `lateinit` does not support. Reading the property before any assignment throws `IllegalStateException("Property X should be initialized before get.")`. After you assign a value it behaves like a normal non-null var and can be reassigned. Compared with `lateinit`, the delegate version boxes the value and goes through delegate get/set, so it has slight overhead — prefer `lateinit` for object references and reserve `notNull()` for primitives.

code

kotlin · 12 lines
kotlin
import kotlin.properties.Delegates

class Config {
    var maxRetries: Int by Delegates.notNull() // primitive: lateinit not allowed
}

fun main() {
    val c = Config()
    // c.maxRetries -> IllegalStateException before set
    c.maxRetries = 3
    println(c.maxRetries) // 3
}

go deeper

for a junior

Knows notNull() defers initialization of a non-null var and throws if read too early.

for a middle

Identifies the primitive-type gap vs lateinit and names the IllegalStateException behaviour.

for a senior

Explains the boxing/delegate overhead, the lack of an isInitialized check, and when to prefer lateinit.

for a principal

Frames late-init choices around API/contract clarity, considers constructor injection or nullable+validation alternatives, and the cost of read-before-init bugs in production.

## The problem Sometimes you need a **non-null** property whose value is set *after* the object is constructed — for example a config injected by a framework, or a value set in an `onCreate`/`init` callback. You cannot write `var x: Int` without an initializer, and you do not want a nullable `Int?` that forces `!!`/`?.` everywhere. ## Two solutions - `lateinit var` — language feature; works for **non-primitive, non-nullable** reference types. - `Delegates.notNull<T>()` — a delegate that fills the gap where `lateinit` cannot be used, **most importantly primitives** (`Int`, `Long`, `Double`, `Boolean`, ...), which `lateinit` forbids. ## Behaviour ```kotlin import kotlin.properties.Delegates var threshold: Int by Delegates.notNull() fun main() { // println(threshold) // would throw IllegalStateException: "Property threshold should be initialized before get." threshold = 42 println(threshold) // 42 threshold = 7 // reassignable like any var } ``` - **Read before assignment** → throws `IllegalStateException` with message "Property <name> should be initialized before get." - **After assignment** → behaves like a normal non-null var; can be reassigned freely. ## notNull() vs lateinit — comparison | | `lateinit var` | `Delegates.notNull()` | |---|---|---| | Primitives (Int/Boolean...) | **Not allowed** | **Allowed** | | Reference types | Allowed (preferred) | Allowed but boxes/overhead | | `::prop.isInitialized` check | Yes | No such check | | Mechanism | Backing field, no boxing | Delegate object + boxing | | Read before set | `UninitializedPropertyAccessException` | `IllegalStateException` | ## Guidance - Use `lateinit` for object references — cheaper, supports `.isInitialized`. - Use `Delegates.notNull()` when the type is a **primitive** and you still want non-null late initialization. - Both signal a programming error (read-before-init) loudly rather than returning null.

  • Why use notNull() instead of lateinit for an Int?
    lateinit does not support primitive types; notNull() provides late, non-null initialization for Int/Long/Boolean etc.
  • What exception type is thrown on read-before-set?
    IllegalStateException with message 'Property X should be initialized before get.' (lateinit instead throws UninitializedPropertyAccessException).
  • Can you check whether a notNull() property was initialized, like ::prop.isInitialized?
    No — the isInitialized check only works for lateinit properties, not for Delegates.notNull().

A reserved seat with a name card: it's guaranteed to be filled, but sit down before anyone arrives and you get thrown out.

saying these in an interview costs you the question

  • Claiming notNull() works on val (it requires var)
  • Saying lateinit supports primitives (it does not)
  • Thinking read-before-set returns null instead of throwing
  • Confusing the exception with UninitializedPropertyAccessException (that's lateinit's)
  • Believing ::prop.isInitialized works for notNull()

context