val / var
Kotlin variables: val vs var, type inference, nullability, lateinit, lazy, const, and the rules that make idiomatic Kotlin readable.
Kotlin — variables
EXAMPLE
// ===== val (immutable binding) and var (mutable) =====
fun demo() {
val name = "Alex" // immutable reference
// name = "Sam" // error: val cannot be reassigned
var count = 0
count += 1 // ok
count = 10
// Default to val. Reach for var only when the binding actually changes.
// ===== Explicit types =====
val age: Int = 30
val pi: Double = 3.14
val on: Boolean = true
val ch: Char = 'A'
val s: String = "hi"
val ids: List<Int> = listOf(1, 2, 3)
// ===== Nullability =====
val maybe: String? = null // explicitly nullable
val length = maybe?.length ?: 0 // safe call + Elvis
// !! force-unwrap (use sparingly):
val forced = maybe!!.length // throws NPE if null
// ===== Smart casts =====
val raw: Any = 42
if (raw is Int) {
// raw is smart-cast to Int inside this block
println(raw + 1)
}
// ===== lateinit (var, non-null, deferred init) =====
// Useful for DI / test setup where the value is known to be set before first use.
// lateinit applies to var of non-null reference type (not primitives or nullable).
// ===== by lazy (val, computed on first read, thread-safe) =====
val expensive: String by lazy { computeOnce() }
// ===== const val (compile-time constant on object/companion) =====
// (See top-level/companion below.)
}
fun computeOnce(): String = "hello"
// ===== Top-level / companion constants =====
const val APP_NAME = "shop"
object Config {
const val MAX_RETRIES = 3
}
class Money(val cents: Long, val currency: String = "AUD") {
companion object {
const val DECIMAL_PLACES = 2
val ZERO = Money(0)
}
}
// ===== Destructuring =====
data class Point(val x: Int, val y: Int)
val (x, y) = Point(3, 4)
// ===== Multiple assignment from a map =====
val (k, v) = "a" to 1 // Pair
// ===== let / also / apply / run / with: scope functions =====
val emailLength = "alex@example.com".let { it.length } // 16
// ===== Read-only collections vs MutableXxx =====
val list = listOf(1, 2, 3) // List<Int> — read-only
val mList = mutableListOf(1, 2) // MutableList<Int>
// You can reassign val on read-only collection? No, val locks the binding.
// You CAN mutate a MutableList via val (mList.add(3) works); the binding is locked, not the object.
// ===== When to use var =====
// - Loop counters (although ranges + map handle most cases)
// - Truly mutable UI state
// - Hot-path code where allocation matters
// Otherwise: val, val, val.
// ===== Patterns to internalise =====
// - Default to val + immutable collections
// - Use type inference; annotate when the right-hand side is opaque
// - String? where null is meaningful; non-null otherwise
// - by lazy for expensive, single-init values
// - data class for value-shaped types; equals/hashCode/copy come free
// ===== Pitfalls =====
// - !! everywhere -> nullability lost; convert to ? + ?: or let
// - lateinit on a primitive or nullable type -> compile error
// - const val on a non-primitive / non-companion -> compile error
// - Mutating an object through a val binding and assuming the binding is 'frozen'
// - lazy without a thread-safety mode in concurrent code (default is LazyThreadSafetyMode.SYNCHRONIZED, which is correct but slow)
Why it matters
val first, var only when you must, nullability explicit, by lazy for one-shot expensive values. Once these are reflex, Kotlin code reads close to a domain model: data classes for shapes, immutable collections by default, scope functions for chained transforms.
Tip: Tweak the snippet with Try it Yourself », then sit the quiz at the bottom of the page.
Example
Exercise
Read-only binding.
pi = 3.14
Three letters.
Discussion
Loading…