Part I — Basics
1 Getting started

Hello, Kotlin

Every program starts with main — the entry point the runtime calls.

Hello.kt
fun main() {
    println("Hello, Kotlin!")
}
Hello, Kotlin!

println prints a line and adds a newline. print does not.

You can also receive command-line arguments:

Kotlin
fun main(args: Array<String>) {
    println("Args: ${args.joinToString()}")
}
2 Variables

val and var

Kotlin
val language = "Kotlin"
var year = 2011
year = 2026
println("$language — $year")
Kotlin — 2026

Type can be explicit: val name: String = "Ada". Usually the compiler infers it.

Rule: Start with val. Switch to var only when you must reassign.
3 Basic types

Numbers, Booleans, Characters

Kotlin
val a = 10
val b = 3.14
val big = 1_000_000_000L
val ok = true
val ch = 'K'
println(a + 5)
println(b * 2)
15 6.28

Underscores improve readability: 1_000_000.

Type conversion

No implicit widening. Convert explicitly:

Kotlin
val i: Int = 42
val d: Double = i.toDouble()
val s: String = i.toString()
val back = "42".toInt()
val safe = "x".toIntOrNull()  // null
4 Strings

Templates, operations, multi-line

String templates

Kotlin
val name = "Kotlin"
val v = 2.0
println("Language: $name")
println("Next: ${v + 0.21}")
Language: Kotlin Next: 2.21

Common operations

Kotlin
val s = "  Hello World  "
println(s.trim())
println(s.uppercase())
println(s.contains("World"))
println(s.replace("World", "Kotlin"))
println(s.substring(2, 7))
println(s.split(" "))
println(s[2])  // char at index

Raw / multi-line strings

Kotlin
val text = """
    |Line one
    |Line two
""".trimMargin()
println(text)

StringBuilder (efficient mutation)

Kotlin
val sb = StringBuilder()
sb.append("Hello").append(" ").append("Kotlin")
println(sb.toString())
5 Operators

Arithmetic, comparison, logical

Kotlin
val x = 10; val y = 3
println(x + y)   // 13
println(x / y)   // 3 (integer division)
println(x % y)   // 1
println(x > y)   // true
println(x == y)  // false  (structural equality)
println(x === y) // referential equality (same instance)
println(x != y)
println(x in 1..20)  // true
13 3 1 true false true true true

Logical: &&, ||, !. Bitwise: and, or, xor, shl, shr.

6 Conditions

if is an expression

Kotlin
val age = 20
val status = if (age >= 18) "adult" else "minor"
println(status)

val max = if (a > b) a else b
adult

No ternary ? : — use if expressions. Chain with else if.

7 Loops & ranges

for, while, ranges

Kotlin
for (i in 1..5) print("$i ")
println()
for (i in 5 downTo 1) print("$i ")
println()
for (i in 0..10 step 2) print("$i ")
println()

var n = 3
while (n > 0) { println(n); n-- }

// break / continue work as expected
// labels: outer@ for (...) { break@outer }
1 2 3 4 5 5 4 3 2 1 0 2 4 6 8 10 3 2 1

until is exclusive upper bound: 0 until 5 → 0..4.

Part II — Functions & Null Safety
8 Functions

Declaration, defaults, named args, varargs

Kotlin
fun greet(name: String): String = "Hello, $name!"

fun connect(host: String = "localhost", port: Int = 8080) {
    println("$host:$port")
}

connect()
connect(port = 3000)
connect("api.example.com", 443)

// vararg
fun sum(vararg nums: Int) = nums.sum()
println(sum(1, 2, 3, 4))

// Unit is the return type of functions that return nothing (like void)
fun log(msg: String) { println(msg) }
localhost:8080 localhost:3000 api.example.com:443 10

Local functions & infix

Kotlin
fun outer() {
    fun inner(x: Int) = x * 2
    println(inner(21))
}

infix fun Int.times(str: String) = str.repeat(this)
println(3 times "Ha")  // HaHaHa

Tail recursion

Kotlin
tailrec fun factorial(n: Int, acc: Int = 1): Int =
    if (n <= 1) acc else factorial(n - 1, n * acc)

println(factorial(5))  // 120
9 Null safety

The type system that kills NPEs

Types are non-null by default. Add ? to allow null.

Kotlin
var a: String = "Hi"     // never null
var b: String? = null    // can be null

// Safe call
println(b?.length)

// Elvis — default when null
val len = b?.length ?: 0

// Not-null assertion (throws if null — use rarely)
// val forced = b!!

// Safe cast
val s = a as? String   // null if cast fails

// let with nullables
b?.let { println("Got: $it") }

lateinit & lazy

Kotlin
lateinit var config: String   // must assign before read (var only)

val expensive by lazy {
    println("Computing…")
    42
}
println(expensive)  // computes once
println(expensive)  // cached
Part III — Collections
10 Lists

Ordered collections

Kotlin
val fruits = listOf("apple", "banana", "cherry")
println(fruits[0])
println(fruits.size)
println(fruits.contains("banana"))

val numbers = listOf(1, 2, 3, 4, 5, 6)
println(numbers.filter { it % 2 == 0 })
println(numbers.map { it * 2 })
println(numbers.sum())
println(numbers.reduce { acc, n -> acc + n })
println(numbers.groupBy { it % 2 == 0 })
println(numbers.take(3))
println(numbers.drop(2))
println(numbers.sortedDescending())
println(numbers.zip(fruits))

val mutable = mutableListOf(1, 2)
mutable.add(3)
mutable[0] = 10
apple 3 true [2, 4, 6] [2, 4, 6, 8, 10, 12] 21 21 {false=[1, 3, 5], true=[2, 4, 6]} [1, 2, 3] [3, 4, 5, 6] [6, 5, 4, 3, 2, 1] [(1, apple), (2, banana), (3, cherry)]
11 Maps

Key–value stores

Kotlin
val ages = mapOf("Ada" to 36, "Grace" to 45)
println(ages["Ada"])
println(ages.getOrDefault("Bob", 0))
println(ages.getOrElse("Bob") { -1 })

for ((name, age) in ages) println("$name → $age")

val m = mutableMapOf<String, Int>()
m["x"] = 1
m.putAll(ages)
println(m.keys)
println(m.values)
println(m.filter { it.value > 40 })
12 Sets

Unique elements

Kotlin
val set = setOf(1, 2, 2, 3)
println(set)                    // [1, 2, 3]
println(set.contains(2))
println(set union setOf(3, 4))
println(set intersect setOf(2, 9))
println(set subtract setOf(1))

val ms = mutableSetOf("a", "b")
ms.add("c")
[1, 2, 3] true [1, 2, 3, 4] [2] [2, 3]
Part IV — Object-Oriented Kotlin
13 Classes

Primary constructor, init, inheritance

Kotlin
class Person(val name: String, var age: Int) {
    init {
        require(age >= 0) { "Age must be ≥ 0" }
        println("Created $name")
    }

    constructor(name: String) : this(name, 0)  // secondary

    fun introduce() = println("I'm $name, $age")
}

val p = Person("Ada", 36)
p.introduce()

// Inheritance — classes are final by default
open class Animal(val name: String) {
    open fun speak() = println("...")
}
class Dog(name: String) : Animal(name) {
    override fun speak() = println("Woof!")
}
Dog("Rex").speak()
Created Ada I'm Ada, 36 Woof!

Abstract classes & nested / inner

Kotlin
abstract class Shape {
    abstract fun area(): Double
    fun description() = "Area = ${area()}"
}

class Outer {
    private val x = 1
    class Nested { fun foo() = 2 }          // no access to Outer
    inner class Inner { fun foo() = x }     // has Outer reference
}
14 Properties

Getters, setters, backing fields

Kotlin
class Rectangle(val width: Int, val height: Int) {
    val area: Int
        get() = width * height          // computed

    var name: String = ""
        get() = field.uppercase()      // field = backing field
        set(value) {
            field = value.trim()
        }
}

val r = Rectangle(3, 4)
println(r.area)   // 12
r.name = "  box  "
println(r.name)   // BOX

Custom accessors can have different visibility. Use field only inside accessors.

15 Visibility modifiers

public, internal, protected, private

Kotlin
internal class ApiClient
private fun helper() {}

open class Base {
    protected val secret = 42
}
class Child : Base() {
    fun show() = println(secret)
}
16 Data classes

equals, hashCode, toString, copy — free

Kotlin
data class User(val id: Int, val name: String)

val u1 = User(1, "Ada")
val u2 = User(1, "Ada")
println(u1)
println(u1 == u2)                 // true
println(u1.copy(name = "Grace"))
val (id, name) = u1                // destructuring
User(id=1, name=Ada) true User(id=1, name=Grace)

Requirements: at least one primary constructor parameter, cannot be abstract/open/sealed/inner.

17 Enums

Enumerated types with behavior

Kotlin
enum class Direction {
    NORTH, SOUTH, EAST, WEST
}

enum class Color(val rgb: Int) {
    RED(0xFF0000),
    GREEN(0x00FF00),
    BLUE(0x0000FF);

    fun hex() = "#${rgb.toString(16).padStart(6, '0')}"
}

println(Direction.NORTH)
println(Color.RED.hex())
println(Color.valueOf("GREEN"))
Color.entries.forEach { println(it) }
NORTH #ff0000 GREEN RED GREEN BLUE
18 object, companion, object expressions

Singletons and anonymous objects

Kotlin
// Singleton
object Logger {
    fun log(msg: String) = println("[LOG] $msg")
}
Logger.log("started")

// Companion object — like static members
class User private constructor(val id: Int) {
    companion object {
        fun create(id: Int) = User(id)
        const val MAX = 100
    }
}
val u = User.create(1)

// Object expression (anonymous class)
val listener = object : MouseAdapter() {
    override fun mouseClicked(e: MouseEvent) { /* … */ }
}
19 Sealed classes & interfaces

Restricted hierarchies + exhaustive when

Kotlin
sealed class Result<out T> {
    data class Success<T>(val data: T) : Result<T>()
    data class Error(val message: String) : Result<Nothing>()
    object Loading : Result<Nothing>()
}

fun handle(r: Result<String>) = when (r) {
    is Result.Success -> println(r.data)
    is Result.Error   -> println(r.message)
    Result.Loading     -> println("…")
}  // no else — exhaustive

// Sealed interface (Kotlin 1.5+)
sealed interface Expr
data class Const(val n: Int) : Expr
data class Plus(val a: Expr, val b: Expr) : Expr

All direct subclasses must be in the same package/module. Ideal for state machines and result types.

20 Interfaces

Contracts with default implementations

Kotlin
interface Clickable {
    fun click()
    fun showOff() = println("I'm clickable")  // default
}

interface Focusable {
    fun showOff() = println("I'm focusable")
}

class Button : Clickable, Focusable {
    override fun click() = println("clicked")
    override fun showOff() {
        super<Clickable>.showOff()
        super<Focusable>.showOff()
    }
}

Button().showOff()
I'm clickable I'm focusable

Interfaces can contain abstract properties and default method bodies. A class may implement many interfaces.

21 when expression

Powerful pattern matching

Kotlin
fun describe(x: Any) = when (x) {
    0            -> "zero"
    in 1..9      -> "single digit"
    is String     -> "string len ${x.length}"
    is Int        -> "int"
    else          -> "other"
}

// without argument — acts like if-else chain
val result = when {
    x < 0  -> "negative"
    x == 0 -> "zero"
    else   -> "positive"
}
Part V — Functional Features
22 Extension functions & properties

Add members without inheritance

Kotlin
fun String.shout() = uppercase() + "!"
val String.lastChar: Char
    get() = this[length - 1]

println("hello".shout())
println("Kotlin".lastChar)

// Nullable receiver
fun String?.isNullOrBlank() = this == null || isBlank()
HELLO! n

Resolved statically. They do not override members — a real member always wins.

23 Lambdas & higher-order functions

Functions as values

Kotlin
val add: (Int, Int) -> Int = { a, b -> a + b }
println(add(2, 3))

fun operate(x: Int, y: Int, op: (Int, Int) -> Int) = op(x, y)
println(operate(4, 5) { a, b -> a * b })

// Trailing lambda + implicit it
listOf(1, 2, 3).forEach { println(it) }

// Function references
fun triple(n: Int) = n * 3
println(listOf(1, 2).map(::triple))

// Returning from lambda — use labels
listOf(1, 2, 3).forEach {
    if (it == 2) return@forEach
    println(it)
}
24 Scope functions

let, run, with, apply, also

Function Object ref Returns Typical use
letitlambda resultnull checks, transform
runthislambda resultcompute on object
withthislambda resultnon-extension variant of run
applythisobjectconfigure / builder
alsoitobjectside effects / logging
Kotlin
val name: String? = "Ada"
name?.let { println("Hi $it") }

val list = mutableListOf<Int>().apply {
    add(1); add(2)
}

val s = "kotlin".run { uppercase().reversed() }
println(s)  // NILTOK
Part VI — Advanced Language Features
25 Generics

Type parameters, variance, reified

Kotlin
class Box<T>(val value: T)

fun <T> singletonList(item: T) = listOf(item)

// Upper bounds
fun <T : Comparable<T>> max(a: T, b: T) = if (a > b) a else b

// Variance
// out T  = covariant  (producer) — List<out T>
// in T   = contravariant (consumer)
interface Producer<out T> { fun produce(): T }
interface Consumer<in T> { fun consume(item: T) }

// Reified type parameters (only with inline)
inline fun <reified T> isType(value: Any) = value is T
println(isType<String>("hi"))  // true

// Star projection
val anyList: List<*> = listOf(1, "a")

Use out when the type only appears in output positions, in for input positions (PECS: Producer Extends, Consumer Super).

26 Delegation

by — class & property delegation

Kotlin
interface Base { fun print() }
class BaseImpl(val x: Int) : Base {
    override fun print() = println(x)
}

// Class delegation
class Derived(b: Base) : Base by b

Derived(BaseImpl(10)).print()

// Property delegation
class Example {
    var p: String by Delegates.observable("init") { _, old, new ->
        println("$old → $new")
    }
    val lazyVal: String by lazy { "computed" }
    var mapProp: String by mutableMapOf()  // map delegation
}
10

Standard delegates: lazy, observable, vetoable, map delegation.

27 Inline functions

Avoid lambda allocations + reified types

Kotlin
inline fun <T> measure(block: () -> T): T {
    val start = System.nanoTime()
    val result = block()
    println("Took ${System.nanoTime() - start} ns")
    return result
}

// noinline — keep a lambda as object
inline fun foo(noinline cb: () -> Unit) { /* store cb */ }

// crossinline — forbid non-local returns in lambda
inline fun runSafe(crossinline body: () -> Unit) {
    try { body() } catch (e: Exception) {}
}

// Reified (only possible with inline)
inline fun <reified T> List<*>.filterIsInstance() =
    filter { it is T }.map { it as T }

The compiler inlines the function body at call sites, eliminating the cost of lambda objects.

28 Operator overloading

Make your types work with +, [], in, …

Kotlin
data class Point(val x: Int, val y: Int) {
    operator fun plus(o: Point) = Point(x + o.x, y + o.y)
    operator fun unaryMinus() = Point(-x, -y)
    operator fun get(index: Int) = if (index == 0) x else y
    operator fun invoke() = println("($x, $y)")
}

val p = Point(1, 2) + Point(3, 4)
println(p)        // Point(x=4, y=6)
println(-p)
println(p[0])
p()               // invoke
Point(x=4, y=6) Point(x=-4, y=-6) 4 (4, 6)

Other operators: minus, times, div, rem, contains (in), compareTo, rangeTo, inc/dec, etc.

29 Exceptions

try as expression, Nothing, checked exceptions

Kotlin
fun parse(s: String): Int =
    try {
        s.toInt()
    } catch (e: NumberFormatException) {
        0
    } finally {
        println("done")
    }

println(parse("42"))
println(parse("x"))

// throw returns Nothing (type that never completes)
fun fail(msg: String): Nothing {
    throw IllegalStateException(msg)
}

val s: String = name ?: fail("name required")

// Kotlin does not have checked exceptions
// @Throws is only for Java interop
done 42 done 0
30 Sequences

Lazy collection processing

Iterable operations are eager (each step creates a full collection). Sequence is lazy — intermediate results are not materialized until a terminal operation.

Kotlin
val result = (1..1_000_000)
    .asSequence()
    .map { it * it }
    .filter { it % 2 == 0 }
    .take(5)
    .toList()

println(result)

// Generate infinite sequences
val fib = generateSequence(1 to 1) { (a, b) -> b to a + b }
    .map { it.first }
    .take(10)
    .toList()
println(fib)
[4, 16, 36, 64, 100] [1, 1, 2, 3, 5, 8, 13, 21, 34, 55]

Use sequences for large or infinite data, or when many intermediate steps would otherwise allocate heavily.

31 Packages & imports

Organization and visibility

Kotlin
package com.example.app

import kotlin.math.PI
import kotlin.math.sqrt as squareRoot
import com.example.util.*          // wildcard

fun main() {
    println(PI)
    println(squareRoot(16.0))
}

Default package is used if none is declared. File names need not match class names (unlike Java).

32 Type aliases

Give existing types a new name

Kotlin
typealias UserId = String
typealias Handler = (String) -> Unit
typealias StringMap = Map<String, String>

fun process(id: UserId, onDone: Handler) {
    onDone(id)
}

Aliases are erased at compile time — they do not create new types. Useful for readability and long function types.

33 Value classes (inline classes)

Zero-cost wrappers

Kotlin
@JvmInline
value class Password(private val s: String) {
    init { require(s.length >= 8) }
    override fun toString() = "*".repeat(s.length)
}

val p = Password("secret123")
println(p)  // *********
// At runtime this is often just a String — no allocation

Value classes must have a single property in the primary constructor. They provide type safety without runtime overhead in many cases.

34 Annotations

Metadata for the compiler and tools

Kotlin
@Target(AnnotationTarget.CLASS, AnnotationTarget.FUNCTION)
@Retention(AnnotationRetention.RUNTIME)
annotation class Auth(val role: String = "user")

@Auth("admin")
class AdminPanel

// Common standard annotations
@JvmStatic          // expose as static to Java
@JvmOverloads       // generate overloads for defaults
@JvmField           // expose as field, no getter
@Throws(IOException::class)
@Deprecated("Use newApi()", ReplaceWith("newApi()"))
@Suppress("UNCHECKED_CAST")
@OptIn(ExperimentalStdlibApi::class)

Annotations can have constructors and be applied to classes, functions, properties, parameters, expressions, and more.

Part VII — Coroutines & Async
35 Coroutines

Structured concurrency & suspend functions

build.gradle.kts
dependencies {
    implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.9.0")
}
Kotlin
import kotlinx.coroutines.*

suspend fun fetch(id: Int): String {
    delay(500)                // non-blocking
    return "Item-$id"
}

fun main() = runBlocking {
    println("Start")

    // Concurrent
    val a = async { fetch(1) }
    val b = async { fetch(2) }
    println("${a.await()} + ${b.await()}")

    // Fire-and-forget
    launch {
        delay(100)
        println("background")
    }

    // Structured scope
    coroutineScope {
        launch { delay(50); println("child") }
    }

    println("Done")
}
Start Item-1 + Item-2 child background Done

Dispatchers & context

Kotlin
launch(Dispatchers.Default) { /* CPU-bound */ }
launch(Dispatchers.IO) { /* blocking IO */ }
withContext(Dispatchers.Default) {
    // switch context temporarily
}

// Cancellation is cooperative
val job = launch {
    while (isActive) {
        ensureActive()
        // work
    }
}
job.cancel()
36 Flows

Cold asynchronous streams

Kotlin
import kotlinx.coroutines.*
import kotlinx.coroutines.flow.*

fun numbers(): Flow<Int> = flow {
    for (i in 1..5) {
        delay(100)
        emit(i)
    }
}

fun main() = runBlocking {
    numbers()
        .map { it * it }
        .filter { it % 2 == 0 }
        .collect { println(it) }
}

// Common operators: map, filter, transform, take,
// flatMapConcat / Merge / Latest, debounce, buffer,
// catch, onCompletion, flowOn, shareIn, stateIn
4 16

Flows are cold (start when collected) and respect cancellation. Use StateFlow / SharedFlow for hot streams and UI state.

Go deeper: Official Coroutines Guide covers channels, select, testing, and multiplatform.

You’ve covered the language

These 36 lessons span the vast majority of everyday and advanced Kotlin. What remains is mostly the standard library surface area, multiplatform specifics, and ecosystem libraries.