Lecture notes — Kotlin Basics
ver. 1.0.0
Where we are
Kotlin: What and Why was a survey. It showed a short program, named the features that made it short, and gave the philosophy behind them. It taught almost no syntax.
This unit teaches the syntax. It is the widest unit of the module, and everything after it assumes all of it.
A theme runs through the whole thing, and it is worth watching for as it recurs: Kotlin turns statements into expressions. A function body can be an expression. when returns a value. try returns a value. Each time this happens, a mutable variable becomes unnecessary — which is the previous unit’s preference for immutability showing up in the syntax rather than in the philosophy.
What you will be able to do
declare-functions— Declare functions with block and expression bodies, and know when the return type can be omitted.choose-val-or-var— Choose between val and var, and say what val does and does not guarantee.use-string-templates— Embed variables and expressions in strings with templates.declare-classes-and-properties— Declare a class with properties, and explain what the compiler generates.write-custom-accessors— Write a property whose value is computed rather than stored.use-enums-and-when— Declare enum classes and match on them with when.use-when-without-an-argument— Use when with no argument as a chain of boolean conditions.apply-smart-casts— Rely on smart casts after an is check instead of casting explicitly.iterate-with-ranges-and-loops— Iterate with while and for, over ranges, progressions, collections and maps.handle-exceptions-as-expressions— Throw and catch exceptions, and use try as an expression.
What we will cover
- Basic elements — functions, variables, string templates.
- Classes and properties — and how much of a Java class the compiler writes for you.
- Enums and
when— Kotlin’s replacement forswitch, and what it can do thatswitchcannot. - Smart casts — the check that removes the cast.
- Loops —
while,for-in, ranges, progressions, and the two jobs ofin. - Exceptions — no checked exceptions, and
tryas an expression.
Hello, world
Tradition first, because a great deal is visible in five lines:
fun main(args: Array<String>) {
println("Hello, world!")
}Five observations, and each is a small design decision:
fundeclares a function, and functions can live at the top level — no enclosing class is required- parameter type follows the name, separated by a colon, which is the ML convention rather than the C one
Array<String>is an ordinary generic type; arrays are not special-cased in the syntaxprintlnrather thanSystem.out.println— Kotlin’s standard library wraps the noisier Java calls- no semicolon at the end of the line
The type-after-name order is not arbitrary. It makes the type optional without changing anything else: drop : Int and the declaration still parses. In C-style order, removing the type leaves nothing to say the line is a declaration at all.
Functions

fun keyword, the name, the parameter list, and the return type.A function with a block body looks like Java’s, with the return type declared after the parameter list:
fun max(a: Int, b: Int): Int {
return if (a > b) a else b
}Note what is already happening in the body: if is an expression, not a statement. It has a value, so it can be returned directly. Kotlin has no ternary operator because it does not need one — if already does that job.
Since the body is a single expression, the braces and return can go entirely:
fun max(a: Int, b: Int): Int = if (a > b) a else bThat is an expression body, and it is the idiomatic form when a function computes one thing. Then the return type can be dropped too:
fun max(a: Int, b: Int) = if (a > b) a else bThe rule, stated precisely
This asymmetry is not arbitrary:
- an expression-bodied function may omit its return type, because the compiler infers it from the single expression
- a block-bodied function must declare it
The reason is that inferring the return type of a block body would mean analysing every path through it and unifying the results. Kotlin’s designers judged that a function complicated enough to need a block is a function whose return type should be written down for the reader.
A function that is an expression is easier to reason about than one that assembles a result over several statements. There is nothing to trace — the body is the answer.
This is the same shift the module keeps making, and it is why you will see far more = than { in idiomatic Kotlin.
Learning outcomes
- declare-functions: Declare functions with block and expression bodies, and know when the return type can be omitted.
Concepts
- expression-body-functions:
=in place of braces, and why only that form may infer its return type - type-inference: the compiler works out the type so you do not have to write it
Variables
Two keywords:
val answer = 42 // read-only
var counter = 0 // mutable
counter = 1 // fineThe type may be written when inference cannot help, or when you want something other than the obvious:
val answer: Int = 42
val yearsToCompute = 7.5e6 // Double, inferred
val n: Long = 7 // stated, because 7 alone would be an IntA val declared without an initialiser is legal as long as it is assigned exactly once before use, on every path.
Prefer val
The book’s advice is to declare everything with val by default and change to var only when you find you must. It is worth taking seriously, and not because immutability is fashionable: a val is a fact about the rest of the function. You read the declaration and you know the value for the whole scope, without scanning for reassignments.
What val actually guarantees
The trap to get right immediately, because it catches everyone once:
val languages = arrayListOf("Java")
languages.add("Kotlin") // this is fine
languages = arrayListOf("Scala") // this is notval prevents reassignment of the reference. It says nothing about whether the object being referenced can change.
Immutability of the reference and immutability of the object are separate, and Kotlin gives them to you separately.
val is the first. The read-only collection interfaces in the type system unit are the second — and that unit shows that even they are weaker than they look.
Confusing the two produces the surprised bug report: “I declared it val and it changed anyway.”
Learning outcomes
- choose-val-or-var: Choose between val and var, and say what val does and does not guarantee.
Concepts
- val-and-var: read-only reference versus mutable one, and what read-only does not cover
String templates
fun main(args: Array<String>) {
val name = if (args.size > 0) args[0] else "Kotlin"
println("Hello, $name!")
}$name interpolates the variable. For anything more than a bare name, use braces:
println("Hello, ${args[0]}!")
println("The first is ${if (args.size > 0) args[0] else "someone"}")The second line shows something worth noticing: an expression inside ${} may itself contain a string with its own quotes. The nesting is handled properly.
Why templates beat concatenation
The reference inside a template is checked at compile time. Misspell $nmae and the code does not compile. Concatenate a misspelled variable and — in a dynamically typed language, at least — you get an odd string at runtime.
To print a literal dollar sign, escape it: \$.
Learning outcomes
- use-string-templates: Embed variables and expressions in strings with templates.
Concepts
- string-templates: interpolation that the compiler checks
Classes and properties
Start with the comparison, because it makes the point faster than any explanation. Here is a Java class:
public class Person {
private final String name;
public Person(String name) { this.name = name; }
public String getName() { return name; }
}And the Kotlin equivalent:
class Person(val name: String)One line. Nothing was given up — the field is there, the getter is there, and Java code calling getName() works exactly as before.
A class with nothing but this kind of declaration is called a value object, and languages that make them expensive to write tend to end up with fewer of them than they should have.
Why the compiler can do this
In Java a property is a convention: a field, plus accessors named according to a pattern that tools recognise. Nothing in the language knows about it.
In Kotlin a property is a language feature. Declaring one generates:
- a private field to hold the value
- a getter
- a setter, but only if it is a
var
class Person(
val name: String, // field + getter
var isMarried: Boolean // field + getter + setter
)And from Kotlin you use it as a property rather than calling the accessors:
val person = Person("Bob", true)
println(person.name) // calls getName() underneath
person.isMarried = false // calls setIsMarried() underneathThe accessors exist; you just do not have to write them or call them by name.
Custom accessors
A property does not have to store anything. Give it a getter with a body and its value is computed on each access:
class Rectangle(val height: Int, val width: Int) {
val isSquare: Boolean
get() = height == width
}There is no field behind isSquare. Each read runs the comparison.
Both work, and the choice is about what you are telling the reader.
- a property describes a characteristic of the object — something it is
- a function describes an action it performs, or a computation with real cost
isSquare is a characteristic. calculateShippingCost() is not, and dressing it up as a property would suggest a cheap field read where an expensive call actually happens.
The rule of thumb: if a reader would be surprised that accessing it does work, make it a function.
Source layout
Packages and imports work much as in Java, with one relaxation: Kotlin does not require the directory structure to match the package structure. Several classes may share a file, and a file may be placed anywhere.
In practice, following the Java convention remains sensible on large projects — but for small groups of related declarations, putting them in one file is idiomatic rather than sloppy.
Learning outcomes
- declare-classes-and-properties: Declare a class with properties, and explain what the compiler generates.
- write-custom-accessors: Write a property whose value is computed rather than stored.
Concepts
- properties-and-accessors: a property is a language concept, and the accessors are generated from it
Enums and when
enum class Color {
RED, ORANGE, YELLOW, GREEN, BLUE, INDIGO, VIOLET
}enum class is two keywords, and enum is a soft keyword — it has meaning only before class, so a variable may still be called enum.
Unusually, an enum may carry constructor parameters, properties and methods like any other class:
enum class Color(val r: Int, val g: Int, val b: Int) {
RED(255, 0, 0), ORANGE(255, 165, 0),
YELLOW(255, 255, 0), GREEN(0, 255, 0),
BLUE(0, 0, 255), INDIGO(75, 0, 130),
VIOLET(238, 130, 238); // the semicolon is required here
fun rgb() = (r * 256 + g) * 256 + b
}That semicolon after the last constant is the only place in Kotlin where one is mandatory. It separates the constant list from the member declarations.
The point of the example is that an enum constant can hold data, not merely a name.
when, and three ways it beats switch
fun getMnemonic(color: Color) =
when (color) {
Color.RED -> "Richard"
Color.ORANGE -> "Of"
Color.YELLOW -> "York"
else -> "..."
}Three differences from Java’s switch, and each changes how you write code:
- it is an expression returning a value, so the whole function is one expression body
- there is no
break, and therefore no fall-through bug - branches match arbitrary objects, not only constants
Multiple values share a branch with commas:
when (color) {
Color.RED, Color.ORANGE, Color.YELLOW -> "warm"
Color.GREEN -> "neutral"
else -> "cold"
}Matching arbitrary objects
This is the substantive difference, and switch cannot express it at all:
fun mix(c1: Color, c2: Color) =
when (setOf(c1, c2)) {
setOf(RED, YELLOW) -> ORANGE
setOf(YELLOW, BLUE) -> GREEN
setOf(BLUE, VIOLET) -> INDIGO
else -> throw Exception("Dirty color")
}The subject is a set, and the branches are sets. when compares with ==, which for a set means order does not matter — so mix(RED, YELLOW) and mix(YELLOW, RED) both hit the first branch, with no extra cases written.
Blocks as branches
A branch may be a block, in which case the last expression in the block is its value:
when (color) {
Color.RED -> {
println("Matched red")
"Richard" // this is the branch's value
}
else -> "..."
}That rule — the last expression of a block is its value — is consistent across Kotlin, and it is worth internalising here where it first appears. It is what lets you add a logging line to a branch without disturbing what the branch returns.
when (setOf(c1, c2)) allocates a Set on every call, and compares against a freshly allocated set in each branch it tries. For a function called in a loop that is real work.
The next section’s argument-less when is one reason it exists.
Learning outcomes
- use-enums-and-when: Declare enum classes and match on them with when.
Concepts
- when-expression: an expression, no fall-through, and matching on arbitrary objects
when without an argument
Drop the subject and each branch becomes a boolean condition. The first true one wins:
fun mixOptimized(c1: Color, c2: Color) =
when {
(c1 == RED && c2 == YELLOW) ||
(c1 == YELLOW && c2 == RED) -> ORANGE
(c1 == YELLOW && c2 == BLUE) ||
(c1 == BLUE && c2 == YELLOW) -> GREEN
else -> throw Exception("Dirty color")
}This is less readable than the set version and allocates nothing. That is the trade, stated plainly: the elegant version costs objects on every call, the fast version costs a reader’s patience.
The honest guidance is to write the first one and reach for the second when a profiler tells you to.
Learning outcomes
- use-when-without-an-argument: Use when with no argument as a chain of boolean conditions.
Concepts
- when-expression: with no subject,
whenis an if/else chain that returns a value
Smart casts
Build a tiny expression hierarchy — a number, and a sum of two expressions:
interface Expr
class Num(val value: Int) : Expr
class Sum(val left: Expr, val right: Expr) : ExprSum holds two Expr values, so the structure nests:

Sum(Sum(Num(1), Num(2)), Num(4)).Now evaluate it. In Java the shape would be: check the type with instanceof, then cast, then use it. The check and the cast state the same fact twice.
Kotlin removes the second statement:
fun eval(e: Expr): Int {
if (e is Num) {
val n = e as Num // this cast is redundant
return n.value
}
if (e is Sum) {
return eval(e.right) + eval(e.left) // e is a Sum here, no cast
}
throw IllegalArgumentException("Unknown expression")
}After if (e is Sum), the compiler knows e is a Sum within that branch, and lets you use its members directly. That is a smart cast. The IDE even shades the background of a smart-cast variable, so you can see where it is happening.
The two conditions
Both follow from what the compiler can actually guarantee:
- the variable must not have changed between the check and the use — automatic for a
val, and not guaranteed for avaror for a property with a custom getter - it applies within the scope where the check is known to hold
If the compiler cannot prove the value is stable, it refuses the smart cast and asks for an explicit one. That refusal is information, not an inconvenience.
Refactoring with when
if is an expression, so the evaluator can lose its return statements:
fun eval(e: Expr): Int =
if (e is Num) e.value
else if (e is Sum) eval(e.right) + eval(e.left)
else throw IllegalArgumentException("Unknown expression")And an if/else chain that tests one subject is exactly what when is for:
fun eval(e: Expr): Int =
when (e) {
is Num -> e.value
is Sum -> eval(e.right) + eval(e.left)
else -> throw IllegalArgumentException("Unknown expression")
}Compare the first version with the last. The when version is not merely shorter — it has no intermediate results, no return statements and no local variables. The smart casts still apply inside the branches.
Adding logging is where the block-branch rule earns its place:
fun evalWithLogging(e: Expr): Int =
when (e) {
is Num -> {
println("num: ${e.value}")
e.value
}
is Sum -> {
val left = evalWithLogging(e.left)
val right = evalWithLogging(e.right)
println("sum: $left + $right")
left + right
}
else -> throw IllegalArgumentException("Unknown expression")
}Learning outcomes
- apply-smart-casts: Rely on smart casts after an is check instead of casting explicitly.
- use-enums-and-when: Declare enum classes and match on them with when.
Concepts
- smart-casts: after a type check the compiler already knows, so the cast is not asked for again
Loops, ranges and in

while tests before the body; do-while runs the body once first.while and do-while work exactly as in Java. This is the one place in the unit where Kotlin adds nothing at all.
for is a different matter. Kotlin has no C-style for with an initialiser, condition and update. There is only for-in, over a range or anything iterable.
That is a deliberate narrowing. The three-clause loop is a reliable source of off-by-one errors, and nearly everything it does is covered by ranges.
Ranges and progressions
val oneToTen = 1..10A range is closed — the end is included. This differs from most languages and is the thing to remember.
Three ways to shape one:
for (i in 100 downTo 1 step 2) { } // 100, 98, ..., 2
for (i in 0 until 10) { } // 0..9 — end excluded
for (i in 1..100) { } // 1..100 — end includeduntil exists precisely because the closed default is wrong for the common “n items” case, where 0 until size says what you mean and 0..size - 1 does not.
FizzBuzz is the book’s exercise, and running it is the fastest way to fix which is which:
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))Note that when did the whole job. No if, no return, no mutable state.
Iterating maps and collections
The for loop can destructure each entry directly in the loop header:
val binaryReps = TreeMap<Char, String>()
for ((letter, binary) in binaryReps) {
println("$letter = $binary")
}The body never mentions entry.key. The same works with an index:
for ((index, element) in list.withIndex()) {
println("$index: $element")
}That is what replaces the C-style loop when you genuinely need the index — and it is safer, because the index cannot get out of step with the element.
This is destructuring’s first appearance, and it will not be explained properly until the operator conventions unit — where it turns out that for ((a, b) in ...) works because of generated component1() and component2() functions, which you can implement on your own types.
in does two jobs
fun isLetter(c: Char) = c in 'a'..'z' || c in 'A'..'Z'
fun isNotDigit(c: Char) = c !in '0'..'9'That is in as a membership test. And:
for (c in "abc") println(c)That is in driving iteration. It also works as a when branch:
fun recognize(c: Char) = when (c) {
in '0'..'9' -> "It's a digit!"
in 'a'..'z', in 'A'..'Z' -> "It's a letter!"
else -> "I don't know…"
}Membership works for anything comparable, not only characters:
println("Kotlin" in "Java".."Scala") // true — alphabetical ordering
println("Kotlin" in setOf("Java", "Scala")) // falseThe same keyword for both jobs is not a coincidence. The operator conventions unit shows that both are conventions backed by operator functions — contains and iterator — which you can implement on your own types to make them work with in and for.
Learning outcomes
- iterate-with-ranges-and-loops: Iterate with while and for, over ranges, progressions, collections and maps.
Concepts
- ranges-and-progressions:
..,downTo,stepanduntil, andinfor membership as well as iteration
Exceptions
The familiar part first:
if (percentage !in 0..100) {
throw IllegalArgumentException(
"A percentage value must be between 0 and 100: $percentage")
}No new keyword, and the exception classes are Java’s. Note in passing that throw is an expression, so it can appear where a value is required:
val percentage =
if (number in 0..100) number
else throw IllegalArgumentException("...")No checked exceptions
Kotlin does not distinguish checked from unchecked. You are never required to catch an exception or declare it:
fun readNumber(reader: BufferedReader): Int? {
try {
val line = reader.readLine()
return Integer.parseInt(line)
}
catch (e: NumberFormatException) {
return null
}
finally {
reader.close()
}
}In Java, readLine and close both throw IOException, and the method must handle or declare it. Kotlin demands nothing — and the NumberFormatException here is caught because the code has something useful to do about it, not because the compiler insisted.
Checked exceptions were a reasonable idea that did not survive contact with practice. In real Java codebases they are routinely caught and ignored, or declared all the way up the call stack — and both defeat the purpose entirely.
Kotlin’s position is that the guarantee was not worth the ceremony. You may disagree, but notice that the argument is empirical rather than aesthetic.
try as an expression
The more useful difference. try returns a value:
fun readNumber(reader: BufferedReader) {
val number = try {
Integer.parseInt(reader.readLine())
} catch (e: NumberFormatException) {
null
}
println(number)
}The value is the last expression of whichever branch executed — the body if nothing threw, the catch if something did. Exactly the same rule as a when branch.
The consequence is the one this unit keeps producing: no mutable variable. Without it you would declare a var before the try and assign it in both paths, and then the reader has to check that every path really does assign it.
That is the third occasion in this unit where a statement became an expression:
- a function body can be an expression, so no
return whenis an expression, so no assignment in each branchtryis an expression, so novardeclared before the block
Every one removes a mutable variable. This is what the previous unit’s philosophy looks like when it reaches the grammar.
Learning outcomes
- handle-exceptions-as-expressions: Throw and catch exceptions, and use try as an expression.
- choose-val-or-var: Choose between val and var, and say what val does and does not guarantee.
Concepts
- unchecked-exceptions: nothing forces you to catch or declare, and
tryreturns a value
What you can now write
The core syntax is in place. You can declare functions and variables, write classes with computed properties, match with when, rely on smart casts, iterate with ranges, and handle exceptions.
That is enough to write small programs rather than only read them.
Learning outcomes
- declare-functions: Declare functions with block and expression bodies, and know when the return type can be omitted.
- choose-val-or-var: Choose between val and var, and say what val does and does not guarantee.
- use-string-templates: Embed variables and expressions in strings with templates.
- declare-classes-and-properties: Declare a class with properties, and explain what the compiler generates.
- write-custom-accessors: Write a property whose value is computed rather than stored.
- use-enums-and-when: Declare enum classes and match on them with when.
- use-when-without-an-argument: Use when with no argument as a chain of boolean conditions.
- apply-smart-casts: Rely on smart casts after an is check instead of casting explicitly.
- iterate-with-ranges-and-loops: Iterate with while and for, over ranges, progressions, collections and maps.
- handle-exceptions-as-expressions: Throw and catch exceptions, and use try as an expression.
Conclusion
Statements keep turning into expressions, and each time a
vardisappears.Function bodies,
when,if,try, eventhrow. This is the single most useful pattern to carry out of the unit, because it changes how you write rather than only what you can write.valprotects the reference, not the object.valmeans no reassignment. The list it points at can still grow. Two separate guarantees, and only the first isval’s job.A property is a language concept, not a naming convention.
One declaration generates the field and the accessors. Give it a custom getter and it stores nothing at all — but only make it a property if a reader would expect the access to be cheap.
whenbeatsswitchon all three counts.It returns a value, it cannot fall through, and it matches arbitrary objects. Matching on a
setOf(...)is somethingswitchcannot express — at the cost of an allocation per call.Smart casts remove the second half of a redundant pair.
After
if (e is Sum), the compiler knows. It refuses only when it cannot prove the value is stable, and that refusal is information about your code.Kotlin dropped checked exceptions on empirical grounds.
Not because error handling does not matter, but because the mechanism was routinely defeated in practice.
trybeing an expression is the part you will actually use daily.
Where next
The next unit, Defining and Calling Functions, is about making functions pleasant to call: named arguments, default parameter values, top-level functions with no enclosing class, and extension functions — which let you add methods to classes you do not own, and which turn out to be the mechanism behind a great deal of Kotlin’s standard library.