Kotlin Basics

Kotlin

2026-08-21 09:00

Where we are

The previous unit surveyed

It showed a program, named its features, gave the philosophy.

It taught almost no syntax.

This unit teaches the syntax

The widest unit of the module.

Everything after it assumes all of it.

One theme runs through

Kotlin turns statements into expressions.

Function bodies. when. try.

Each time, a mutable variable becomes unnecessary.

What you will be able to do

  1. Declare functions with block and expression bodies.
  2. Choose val or var, and say what val guarantees.
  3. Use string templates.
  4. Declare classes with properties.
  5. Write a computed property.
  6. Declare enums and match with when.
  7. Use when with no argument.
  8. Rely on smart casts.
  9. Iterate with ranges and loops.
  10. Handle exceptions, and use try as an expression.

Hello, world

Five lines, five decisions

fun main(args: Array<String>) {
    println("Hello, world!")
}
  • fun declares a function — at the top level, no class
  • type follows the name, after a colon
  • Array<String> is an ordinary generic type
  • println, not System.out.println
  • no semicolon

Why type-after-name

It makes the type optional without changing anything else.

Drop : Int and the declaration still parses.

In C-style order, there would be nothing left saying it is a declaration.

Functions

The parts

Block body

fun max(a: Int, b: Int): Int {
    return if (a > b) a else b
}

Note what is already happening: if is an expression.

Which is why Kotlin has no ternary operator. It does not need one.

Expression body

fun max(a: Int, b: Int): Int = if (a > b) a else b
fun max(a: Int, b: Int) = if (a > b) a else b

The return type can go too.

The rule, and it is not arbitrary

Expression body → return type may be inferred.

Block body → must be declared.

Inferring a block’s type means analysing every path. A function that complicated should say what it returns.

More than brevity

A function that is an expression is easier to reason about

than one that assembles a result.

There is nothing to trace. The body is the answer.

Variables

Two keywords

val answer = 42          // read-only
var counter = 0          // mutable
val n: Long = 7          // stated, because 7 alone is an Int

Prefer val

Not because immutability is fashionable.

A val is a fact about the rest of the function.

Read the declaration, know the value, stop scanning for reassignments.

The trap

val languages = arrayListOf("Java")
languages.add("Kotlin")            // fine
languages = arrayListOf("Scala")   // not fine

val prevents reassignment of the reference.

It says nothing about the object.

Two separate guarantees

Immutability of the reference — that is val.

Immutability of the object — that is elsewhere, and weaker than it looks.

String templates

Interpolation

println("Hello, $name!")
println("Hello, ${args[0]}!")
println("First is ${if (args.size > 0) args[0] else "someone"}")

Note the nested quotes in the third. Handled properly.

Why templates beat concatenation

The reference is checked at compile time.

Misspell $nmae and the code does not compile.

Classes and properties

Java

public class Person {
    private final String name;
    public Person(String name) { this.name = name; }
    public String getName() { return name; }
}

Kotlin

class Person(val name: String)

Nothing was given up. The field is there. The getter is there.

Java calling getName() works exactly as before.

Why the compiler can do this

In Java, a property is a convention — a field plus accessors named a certain way. Nothing in the language knows about it.

In Kotlin, a property is a language feature.

What one declaration generates

class Person(
    val name: String,        // field + getter
    var isMarried: Boolean   // field + getter + setter
)
println(person.name)          // calls getName()
person.isMarried = false      // calls setIsMarried()

Custom accessors

class Rectangle(val height: Int, val width: Int) {
    val isSquare: Boolean
        get() = height == width
}

No field behind isSquare. Each read runs the comparison.

Property or function?

A property describes what the object is.

A function describes what it does, or a computation with real cost.

If a reader would be surprised that access does work — make it a function.

Source layout

Kotlin does not require directories to match packages.

Several classes per file is idiomatic, not sloppy.

For large projects, follow the Java convention anyway.

Enums and when

Two keywords

enum class Color {
    RED, ORANGE, YELLOW, GREEN, BLUE, INDIGO, VIOLET
}

enum is a soft keyword — meaningful only before class.

With data

enum class Color(val r: Int, val g: Int, val b: Int) {
    RED(255, 0, 0), ORANGE(255, 165, 0),
    /* ... */
    VIOLET(238, 130, 238);      // required semicolon

    fun rgb() = (r * 256 + g) * 256 + b
}

The only place in Kotlin where a semicolon is mandatory.

Three ways when beats switch

It is an expression returning a value.

There is no break, so no fall-through bug.

Branches match arbitrary objects.

The third one is substantive

fun mix(c1: Color, c2: Color) =
    when (setOf(c1, c2)) {
        setOf(RED, YELLOW) -> ORANGE
        setOf(YELLOW, BLUE) -> GREEN
        else -> throw Exception("Dirty color")
    }

The subject is a set. Order does not matter, and no extra cases were written.

Blocks as branches

Color.RED -> {
    println("Matched red")
    "Richard"                 // the branch's value
}

The last expression of a block is its value.

Consistent across Kotlin. Worth internalising here.

A cost hiding in the elegant version

when (setOf(c1, c2)) allocates a Set on every call.

And compares against a freshly allocated set in each branch it tries.

when without an argument

Boolean branches

fun mixOptimized(c1: Color, c2: Color) =
    when {
        (c1 == RED && c2 == YELLOW) ||
        (c1 == YELLOW && c2 == RED) -> ORANGE
        else -> throw Exception("Dirty color")
    }

The trade, stated plainly

The elegant version costs objects on every call.

The fast version costs a reader’s patience.

Write the first. Reach for the second when a profiler says to.

Smart casts

A tiny hierarchy

interface Expr
class Num(val value: Int) : Expr
class Sum(val left: Expr, val right: Expr) : Expr

It nests

Java’s shape

Check with instanceof. Then cast. Then use it.

The check and the cast state the same fact twice.

Kotlin removes the second

if (e is Sum) {
    return eval(e.right) + eval(e.left)   // e is a Sum here
}

The IDE even shades the background where a smart cast is active.

Two conditions

  • the variable must not have changed since the check
  • it applies within the scope where the check holds

A val satisfies the first automatically. A var may not.

When the compiler refuses, that refusal is information.

Refactored with when

fun eval(e: Expr): Int =
    when (e) {
        is Num -> e.value
        is Sum -> eval(e.right) + eval(e.left)
        else -> throw IllegalArgumentException("Unknown expression")
    }

Not merely shorter. No intermediate results, no returns, no locals.

Loops, ranges and in

while, unchanged

There is one for loop

for-in. No C-style initialiser, condition, update.

A deliberate narrowing: the three-clause loop is a reliable source of off-by-one errors.

Ranges are closed

val oneToTen = 1..10        // 10 is included
for (i in 100 downTo 1 step 2) { }   // 100, 98, ..., 2
for (i in 0 until 10) { }            // 0..9

until exists because the closed default is wrong for “n items”.

FizzBuzz, and when did all of it

fun fizzBuzz(i: Int) = when {
    i % 15 == 0 -> "FizzBuzz "
    i % 3 == 0 -> "Fizz "
    i % 5 == 0 -> "Buzz "
    else -> "$i "
}

for (i in 1..100) print(fizzBuzz(i))

No if. No return. No mutable state.

Destructuring in the header

for ((letter, binary) in binaryReps) { }
for ((index, element) in list.withIndex()) { }

The body never mentions entry.key.

And the index cannot get out of step with the element.

in does two jobs

c in 'a'..'z'          // membership
for (c in "abc")       // iteration

Not a coincidence. Both are conventions backed by operator functions —

which you can implement on your own types. Unit 7.

Exceptions

Familiar

throw IllegalArgumentException("A percentage must be 0..100: $percentage")

No new. Java’s exception classes.

And throw is an expression, so it fits where a value is required.

No checked exceptions

You are never required to catch or declare.

readLine and close both throw IOException. Kotlin demands nothing.

An empirical judgement

Checked exceptions are routinely caught and ignored,

or declared all the way up.

Both defeat the purpose. The guarantee was not worth the ceremony.

try as an expression

val number = try {
    Integer.parseInt(reader.readLine())
} catch (e: NumberFormatException) {
    null
}

The value is the last expression of whichever branch ran.

Same rule as a when branch.

Third time

  1. Function body is an expression → no return
  2. when is an expression → no assignment per branch
  3. try is an expression → no var before the block

Every one removes a mutable variable.

Summary

The six things to carry away

  • Statements keep becoming expressions, and each time a var disappears.
  • val protects the reference, not the object.
  • A property is a language concept, not a naming convention.
  • when beats switch on all three counts — and matching a setOf costs an allocation.
  • Smart casts remove the second half of a redundant pair.
  • Checked exceptions were dropped on empirical grounds.

Two forward pointers

Destructuring, met in the map loop — a convention, explained in unit 7.

in, doing double duty — two convention functions, explained in unit 7.

Where next

Defining and Calling Functions is about making functions pleasant to call.

Named arguments, default values, top-level functions.

And extension functions — adding methods to classes you do not own.