Kotlin Basics
Topics: functions · variables · string templates · classes · enums · when · smart casts · loops · exceptions
1. Functions
Syntax
fun max(a: Int, b: Int): Int {
return if (a > b) a else b
}Key points:
- The
funkeyword declares a function. It can live at the top level of any file, with no enclosing class required. - Parameter syntax is
name: Type(name first, type after). - Return type follows the parameter list, after
:.
Expression body
When the body is a single expression, skip the braces and return:
fun max(a: Int, b: Int): Int = if (a > b) a else b
// Return type is optional. The compiler infers it from the expression:
fun max(a: Int, b: Int) = if (a > b) a else bType inference applies only to expression bodies. Functions with a block body that return a value must declare the return type explicitly.
Library authors: keep return types explicit. Inferred types can change silently and break consumers. The compiler's explicit API mode enforces this.
Control flow as expressions
if produces a value in Kotlin, so there is no ternary operator. when and try are expressions too; both are covered in detail in sections 6 and 9.
val x = if (myBoolean) 3 else 5
val direction = when (inputString) {
"u" -> UP
"d" -> DOWN
else -> UNKNOWN
}
val number = try {
inputString.toInt()
} catch (nfe: NumberFormatException) { -1 }Assignments are not expressions. val b = i = getNumber() is a compile error. This eliminates the classic = versus == confusion.
main entry point
fun main() { /* no args */ }
fun main(args: Array<String>) { /* CLI args */ }main returns nothing in either form.
2. Variables
val question: String = "The Answer" // explicit type
val answer = 42 // inferred as Int
var counter = 0 // reassignable| Keyword | Reassignable | Java equivalent |
|---|---|---|
val |
No | final |
var |
Yes | regular field |
Default to val. Switch to var only when mutation is required.
A val reference is read only, but the object it points to can be mutable:
val languages = mutableListOf("Java")
languages.add("Kotlin") // fine. It mutates the list, not the referencevar locks the type, not just the name. After var answer = 42, assigning answer = "no answer" is a type mismatch error.
If you declare a variable without an initializer, you must state its type:
val answer: Int
answer = 42 // assigned later; still val3. String Templates
Prefix a variable with $ to embed its value in a string literal:
val name = "Kotlin"
println("Hello, $name!") // Hello, Kotlin!
println("Hello, ${name.length}-letter name") // Hello, 6-letter name
println("Hello, ${if (name.isBlank()) "someone" else name}!")$variablefor simple identifiers.${expression}for anything more complex, including nested string literals.- Escape a literal dollar sign with
\$. - Templates are checked at compile time. Referencing an undefined variable in
$nameor${…}is a compile error, not a surprise at runtime.
Under the hood, templates compile to ordinary string concatenation; no template parsing happens at runtime.
4. Classes and Properties
Kotlin vs Java boilerplate
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)public is the default visibility — no need to write it.
Properties
Declare with val for a read only property or var for a property that can be written:
class Person(
val name: String, // getter only
var isStudent: Boolean // getter + setter
)Kotlin generates the accessors automatically. From Java, name surfaces as getName(), and isStudent as isStudent() / setStudent() (the is prefix is kept for booleans).
From Kotlin, access properties directly — the compiler routes through the accessor:
val p = Person("Bob", true)
println(p.name) // calls getName() under the hood
p.isStudent = false // calls setStudent(false)Custom accessors
Use a custom getter when a property is derived from others rather than stored:
class Rectangle(val height: Int, val width: Int) {
val isSquare: Boolean
get() = height == width // computed on every access, no backing field
}
val r = Rectangle(41, 43)
println(r.isSquare) // falseRule of thumb: use a property for characteristics, a member function for behavior.
Packages and imports
A package is declared at the top of each file. Here, two files in different packages:
// geometry/shapes/Rectangle.kt
package geometry.shapes
class Rectangle(val height: Int, val width: Int) {
val isSquare get() = height == width // type inferred from the getter
}
fun createUnitSquare() = Rectangle(1, 1)// geometry/example/Main.kt
package geometry.example
import geometry.shapes.Rectangle
import geometry.shapes.createUnitSquare // functions import the same way as classes
fun main() {
println(Rectangle(3, 4).isSquare) // false
println(createUnitSquare().isSquare) // true
}A wildcard import brings in every declaration from the package, and can replace the two explicit imports above:
import geometry.shapes.*Unlike Java, Kotlin doesn't require the directory structure to mirror the package hierarchy, though following the convention helps in projects that mix languages.
5. Enums
enum class Color {
RED, ORANGE, YELLOW, GREEN, BLUE, INDIGO, VIOLET
}enum is a modifier keyword: it only acts as a keyword before class, and can still be used as an identifier elsewhere. class is a hard keyword and can never be an identifier.
Enums can carry properties and methods:
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); // semicolon required before methods
val rgb = (r * 256 + g) * 256 + b
fun printColor() = println("$this is $rgb")
}
Color.GREEN.printColor() // GREEN is 65280The semicolon after the last constant is required whenever the enum body also declares properties or methods. It is the one place Kotlin needs a semicolon in ordinary code spanning multiple lines. You would also need one to put two statements on the same line.
6. when
when is Kotlin's replacement for switch. It is an expression, so it returns a value.
Matching enum constants
fun getMnemonic(color: Color) = when (color) {
Color.RED -> "Richard"
Color.ORANGE -> "Of"
Color.YELLOW -> "York"
Color.GREEN -> "Gave"
Color.BLUE -> "Battle"
Color.INDIGO -> "In"
Color.VIOLET -> "Vain"
}No break is needed. Only the matched branch executes.
Note: the examples below write bare constant names (
RED) instead ofColor.RED. That requiresimport Color.*at the top of the file; otherwise qualify each constant.
Combining branches
fun getWarmth(color: Color) = when (color) {
RED, ORANGE, YELLOW -> "warm"
GREEN -> "neutral"
BLUE, INDIGO, VIOLET -> "cold"
}Capturing the subject in a variable
fun getWarmthFromSensor() =
when (val color = measureColor()) { // color scoped to this when block
RED, ORANGE, YELLOW -> "warm (red = ${color.r})"
GREEN -> "neutral (green = ${color.g})"
BLUE, INDIGO, VIOLET -> "cold (blue = ${color.b})"
}Matching arbitrary objects
when checks branches for equality, so any object works:
fun mix(c1: Color, c2: Color) =
when (setOf(c1, c2)) {
setOf(RED, YELLOW) -> ORANGE
setOf(YELLOW, BLUE) -> GREEN
setOf(BLUE, VIOLET) -> INDIGO
else -> throw IllegalArgumentException("Dirty color")
}setOf(c1, c2) and setOf(RED, YELLOW) are equal regardless of order, because sets ignore order. The else branch is required here because the compiler can't verify exhaustiveness for arbitrary set combinations.
when without argument (boolean branches)
The version using sets allocates a Set on each call. If the function is on a hot path:
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
(c1 == BLUE && c2 == VIOLET) || (c1 == VIOLET && c2 == BLUE) -> INDIGO
else -> throw IllegalArgumentException("Dirty color")
}No argument means each branch is an arbitrary boolean expression. No objects allocated.
Branches can also be blocks; see Block branches in the next section, where the Expr example they rely on is introduced.
7. Smart Casts
After an is check, the compiler automatically casts the variable. No explicit as is needed.
Example: expression evaluator
interface Expr
class Num(val value: Int) : Expr
class Sum(val left: Expr, val right: Expr) : ExprSum(Sum(Num(1), Num(2)), Num(4)) encodes (1 + 2) + 4.
Without smart cast (Java style)
fun eval(e: Expr): Int {
if (e is Num) {
val n = e as Num // explicit cast; redundant
return n.value
}
if (e is Sum) {
val s = e as Sum // explicit cast; redundant
return eval(s.left) + eval(s.right)
}
throw IllegalArgumentException("Unknown expression")
}The compiler already knows the type after each is check, so the casts add nothing.
Idiomatic Kotlin: when + smart cast
fun eval(e: Expr): Int =
when (e) {
is Num -> e.value // e is Num here
is Sum -> eval(e.left) + eval(e.right) // e is Sum here
else -> throw IllegalArgumentException("Unknown expr")
}Block branches
When a when branch needs more than one statement, use a block. The last expression in the block is the branch's result:
fun evalWithLogging(e: Expr): Int =
when (e) {
is Num -> {
println("num: ${e.value}")
e.value // returned
}
is Sum -> {
val left = evalWithLogging(e.left)
val right = evalWithLogging(e.right)
println("sum: $left + $right")
left + right // returned
}
else -> throw IllegalArgumentException("Unknown expression")
}Smart cast requirements
The variable must be a val with no custom accessor — the compiler must guarantee the value can't change between the check and the use.
Explicit cast with as
val n = e as Num // throws ClassCastException if e is not a Num8. Loops
while and do-while
while (condition) {
if (shouldExit) break
}
do {
if (shouldSkip) continue
} while (condition)Labeled break/continue for nested loops:
outer@ while (outerCondition) {
while (innerCondition) {
if (shouldExit) break@outer // exits outer loop
if (shouldSkipOuter) continue@outer // jumps to next outer iteration
}
}for and ranges
Kotlin has no for loop in the C style. Use ranges instead:
for (i in 1..10) { } // 1 to 10 inclusive
for (i in 1..<10) { } // 1 to 9 (upper bound excluded)
for (i in 10 downTo 1) { } // 10, 9, ... 1
for (i in 100 downTo 1 step 2) { } // 100, 98, 96, ...Version note: the range operator
..<, which excludes the upper bound, was introduced as an experimental preview in Kotlin 1.7.20 and became stable in Kotlin 1.9.0. On older codebases, or if you see build errors with..<, use the equivalentuntilfunction:for (i in 1 until 10) { }produces the same sequence from 1 through 9 and has been available since Kotlin 1.0.
Ranges work for characters too:
for (c in 'A'..'F') { /* A B C D E F */ }Iterating over collections and maps
val list = listOf("red", "green", "blue")
for (color in list) { print("$color ") }
// Map destructuring
val binaryReps = mutableMapOf<Char, String>()
for (c in 'A'..'F') {
binaryReps[c] = c.code.toString(radix = 2)
}
for ((letter, binary) in binaryReps) {
println("$letter = $binary")
}
// Index + element
for ((index, element) in list.withIndex()) {
println("$index: $element")
}Map access: use map[key] to read and map[key] = value to write. No get or put calls are needed.
in membership check
fun isLetter(c: Char) = c in 'a'..'z' || c in 'A'..'Z'
fun isNotDigit(c: Char) = c !in '0'..'9'
// c in 'a'..'z' compiles to 'a' <= c && c <= 'z'in works in when branches:
fun recognize(c: Char) = when (c) {
in '0'..'9' -> "digit"
in 'a'..'z', in 'A'..'Z' -> "letter"
else -> "other"
}in also applies to any Comparable:
println("Kotlin" in "Java".."Scala") // true, using lexicographic comparison
println("Kotlin" in setOf("Java", "Scala")) // false, because this checks set membershipCaveat: string ranges compare lexicographically using
String.compareTo, which is case sensitive and compares UTF-16 code units one by one. This is not dictionary ordering."Kotlin" in "Java".."Scala"works here because'J' < 'K' < 'S'holds for the first character, but results can surprise you once case differs:"apple" in "Apple".."Banana"isfalse, because every uppercase ASCII letter sorts before every lowercase one. Do not rely on string ranges for anything intended for users, such as alphabetizing names, without normalizing case first.
9. Exceptions
Throwing
if (percentage !in 0..100) {
throw IllegalArgumentException(
"A percentage value must be between 0 and 100: $percentage"
)
}No new keyword. throw is an expression:
val percentage =
if (number in 0..100) number
else throw IllegalArgumentException("Invalid: $number")try / catch / finally
fun readNumber(reader: BufferedReader): Int? {
try {
val line = reader.readLine()
return Integer.parseInt(line)
} catch (e: NumberFormatException) { // type annotation on the right
return null
} finally {
reader.close()
}
}Int? is a nullable type: the function returns either an Int or null. Here null signals that the input wasn't a number.
Kotlin has no checked exceptions. There is no throws clause on function signatures. You decide which exceptions to handle; the compiler never forces you.
Rationale: Java's checked exceptions often led to boilerplate rethrows and empty catch blocks without improving reliability.
try as an expression
fun readNumber(reader: BufferedReader) {
val number = try {
Integer.parseInt(reader.readLine()) // value if no exception
} catch (e: NumberFormatException) {
null // value if exception caught
}
println(number)
}The value of the try expression is the last expression in the try block (normal path) or in the matching catch block (exception path). Unlike if/when, curly braces around the try body are always required.
Summary
| Feature | Key point |
|---|---|
fun |
Top level, no class needed; an expression body (=) allows the return type to be inferred |
val / var |
Prefer val; type is fixed at declaration, not reassignment |
| String templates | $name, ${expression}. Checked at compile time |
| Properties | Replace fields + getters/setters; val is read only, var is mutable |
| Custom getter | Computed on access via get() = …, no backing field needed |
| Packages / imports | Functions and classes import the same way; directories needn't mirror packages |
| Enums | Can carry properties and methods; semicolon required after the last constant when a body follows |
when |
Expression; matches constants, types, sets, or booleans |
| Smart cast | After is check, no explicit as required |
| Loops / ranges | No C style for; 1..10 (closed), 1..<10 / 1 until 10 (upper bound excluded), downTo, step |
in |
Membership check in ranges, collections, any Comparable (string ranges are case sensitive) |
| Exceptions | No checked exceptions; throw and try are expressions |