Kotlin Fundamentals
Kotlin Fundamentals Null safety, immutability, data classes, extension functions, and scope functions — the parts of Kotlin that come up in every interview and …
Kotlin Fundamentals
Null safety, immutability, data classes, extension functions, and scope functions — the parts of Kotlin that come up in every interview and review.
val vs var, Type Inference
val name = "Alice" // read-only, like `const`
var count = 0 // mutable, like `let`
// val ≠ immutable object — it's a final reference
val list = mutableListOf(1, 2)
list.add(3) // OK — list is mutable, ref is not
// list = mutableListOf() // ERROR — val can't be reassigned
// Use `val` by default. Reach for `var` only when you must mutate.
// Type inference works for almost anything; explicit when ambiguous
val id: Long = 1 // forces Long, not Int
val items: List<String> = emptyList() // type can't be inferred from emptyNull Safety
// Non-nullable by default — won't compile if you assign null
var name: String = "Bob"
// name = null // ERROR
// Opt into nullable with `?`
var nickname: String? = null
// ?. — safe call, returns null instead of NPE
val length: Int? = nickname?.length
// ?: — Elvis, fallback when left side is null
val len: Int = nickname?.length ?: 0
// !! — assert non-null, throws NPE if null (avoid; use only at boundaries)
val forced: Int = nickname!!.length
// let — run block only when non-null
nickname?.let { println("Hi, \$it") }
// Smart cast — compiler narrows the type after a null check
fun greet(user: String?) {
if (user != null) {
println(user.uppercase()) // user is String, not String? here
}
}Data Classes
// Auto-generates equals(), hashCode(), toString(), copy(), componentN()
data class User(val id: Long, val name: String, val email: String? = null)
val u1 = User(1, "Alice")
val u2 = u1.copy(name = "Alicia") // copy with one field changed
u1 == User(1, "Alice") // true — structural equality
u1 === User(1, "Alice") // false — different references
// Destructuring via componentN()
val (id, name, email) = u1
// Use cases: DTOs, API responses, value types, sealed-class variants
// Don't use for entities with identity that survives field changes
// Limitations: data classes can't be `open` or `abstract`; primary
// constructor must declare at least one parameter; only fields in the
// primary constructor participate in equals/hashCode/toString.Extension Functions
// Add methods to classes you don't own — without inheritance
fun String.isValidEmail(): Boolean =
this.matches(Regex("^[\\w.+-]+@[\\w.-]+\\.[a-z]{2,}\$"))
"alice@dev.com".isValidEmail() // true
"nope".isValidEmail() // false
// Extensions are resolved STATICALLY at compile time — not virtual.
// They can't access private members. They're sugar for static functions:
// `String.isValidEmail(this)` is what actually runs.
// Nullable receivers — works on nullable values too
fun String?.orEmpty(): String = this ?: ""
val s: String? = null
s.orEmpty() // "" — no NPE
// Common stdlib examples worth memorising:
// listOf(...).filter { ... }.map { ... } — collection chains
// "hello".uppercase().reversed() — String extensions
// 1.until(10).step(2).toList() — Range buildersScope Functions: let / run / with / apply / also
// All five run a block on an object. They differ in (a) how you refer to
// the object (`it` or `this`) and (b) what the block returns.
//
// Function Receiver Returns Use for
// ───────── ───────── ─────────────── ───────────────────────────────
// let it block result null check, mapping
// run this block result init + compute on one object
// with this block result calling many methods on object
// apply this the object object configuration / builders
// also it the object side effects (log, debug)
// let — null-safe transform
val len: Int? = nickname?.let { it.length }
// apply — configure and return the same object
val intent = Intent().apply {
action = Intent.ACTION_VIEW
putExtra("id", 42)
}
// also — side effect, no transform
val users = fetchUsers().also { println("loaded \${it.size}") }
// run — group setup and return a result
val label = User(1, "Alice").run { "\$id: \$name" }
// with — non-extension version of run, for libraries / external objects
val json = with(StringBuilder()) {
append('{'); append("\"a\":1"); append('}'); toString()
}Sealed Classes & When
// Sealed = closed hierarchy known at compile time. Compiler can verify
// `when` is exhaustive — no `else` branch needed.
sealed class Result<out T> {
data class Success<T>(val value: T) : Result<T>()
data class Failure(val error: Throwable) : Result<Nothing>()
data object Loading : Result<Nothing>()
}
fun render(r: Result<String>): String = when (r) {
is Result.Success -> "OK: \${r.value}" // smart-cast to Success<String>
is Result.Failure -> "Err: \${r.error.message}"
Result.Loading -> "..."
// no `else` — compiler knows the hierarchy is exhaustive
}
// Compare to enums: sealed can carry per-case data; enum cases are singletons.
// Use enum when cases have no state; sealed when they do.