Kotlin
01 / 02

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 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 empty

Null 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 builders

Scope 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.

Keep your own version of these notes — editable, searchable, and organised by your stack.

Start free