programming3 min read

Kotlin Tutorial: Learn Modern JVM from Scratch (2026)

Kotlin Tutorial: Learn Modern JVM from Scratch (2026)

Published:  |  Category: Programming  |  Reading time: ~15 min
Kotlin Tutorial: Learn Modern JVM from Scratch (2026)

Kotlin brought modern language design to the JVM without sacrificing Java interop. I have used it for Android apps, backend services with Ktor, and Gradle plugins. JetBrains designed Kotlin to fix Java's pain points: null safety, verbosity, and limited functional capabilities.

What keeps me coming back is how Kotlin lets you express intent clearly. Extension functions, sealed classes, and coroutines make code read like a specification rather than an implementation.

Coroutines

Coroutines provide structured concurrency without the overhead of threads. A coroutine is a suspendable computation: suspend functions can pause execution without blocking a thread and resume later. launch fires a fire-and-forget coroutine; async returns a Deferred that you can await.

suspend fun fetch(url: String): String {
    return withContext(Dispatchers.IO) {
        URL(url).readText()
    }
}

fun main() = runBlocking {
    val d1 = async { fetch("https://a.com") }
    val d2 = async { fetch("https://b.com") }
    println(d1.await() + d2.await())
}

Null Safety

Kotlin makes null explicit in the type system. Types ending with ? are nullable; all others cannot be null. The safe call operator ?. short-circuits on null. The Elvis operator ?: provides a default. Smart casts narrow types after null checks automatically.

fun greet(name: String?) {
    val display = name ?: "guest"
    println("Hello, $display")
}

fun main() {
    greet(null)
    greet("Alice")
    
    val len: Int? = name?.length
    val sure: Int = name?.length ?: 0
}

Extension Functions

Extensions add functionality to existing classes without inheritance. An extension function is resolved statically based on the declared type, not the runtime type. They are syntactic sugar for static utility methods. Use extensions to enrich third-party library classes or to organize related logic.

fun String.isEmail(): Boolean {
    return this.contains("@") && this.contains(".")
}

fun  List.secondOrNull(): T? {
    return if (size >= 2) this[1] else null
}

fun main() {
    println("a@b.com".isEmail())
    println(listOf(1, 2, 3).secondOrNull())
}

Data Classes

Data classes automatically generate equals(), hashCode(), toString(), copy(), and component functions for destructuring. Declare with data class and list properties in the primary constructor. They are ideal for DTOs, API responses, and value objects.

data class User(
    val id: Long,
    val name: String,
    val email: String
)

fun main() {
    val u = User(1, "Alice", "a@b.com")
    val copy = u.copy(name = "Bob")
    val (id, name) = u
}

Sealed Classes

Sealed classes restrict class hierarchies. All subclasses must be defined in the same file or package. The compiler knows all variants, enabling exhaustive when expressions without else branches. Use sealed classes for state machines, network responses, and UI state models.

sealed class NetworkResult {
    data class Success(val data: T) : NetworkResult()
    data class Error(val msg: String) : NetworkResult()
    data object Loading : NetworkResult()
}

fun handle(r: NetworkResult) = when(r) {
    is NetworkResult.Success -> r.data
    is NetworkResult.Error -> "err: ${r.msg}"
    NetworkResult.Loading -> "loading"
}

Delegation

The delegation pattern is built into the language with the by keyword. by delegates interface implementation or property getters/setters to another object. lazy delegates initialize on first access. observable delegates fire callbacks on property changes.

class CountingSet(
    private val inner: MutableSet = mutableSetOf()
) : MutableSet by inner {
    var addCount = 0

    override fun add(element: T): Boolean {
        addCount++
        return inner.add(element)
    }
}

val lazyVal by lazy { computeExpensive() }

Frequently Asked Questions

Kotlin vs Java?

Kotlin is more concise, has null safety, coroutines, extension functions, and is 100% interop with Java. You can mix both in the same project.

What is a coroutine scope?

Scopes manage coroutine lifecycles. GlobalScope lives forever; viewModelScope ties to Android ViewModel lifecycle. Always use an appropriate scope.

val vs var?

val is read-only (cannot be reassigned but may be mutable internally). var is mutable. Prefer val by default.

How does flow differ from sequences?

Flow is asynchronous and can emit values over time. Sequence is synchronous and blocking. Flow supports coroutine context switching.

Originally published on Ayodhyyya. Last updated June 1, 2026.