Keentune

Kotlin curriculum

24 chapters
·
216 concepts
·
free
Everything the adaptive question bank can teach and test in Kotlin, from foundations through advanced practice. Work through it in order, or start practising and let the questions find your level.
New here? Read the Kotlin guide
A free 18-minute primer — the mental model, the mistakes beginners make, and what to practise first.
A. Basics: declarations, expressions and files
val binds the reference once; the object it points at can still mutate
a type is inferred from the initializer, but a declaration without one must state it
fun f() = expr infers its return type; a block body does not
no-result functions return the Unit object, not void, so they compose as expressions
$name and ${expr} interpolate; a literal $ must be escaped
functions, properties and type aliases can live outside any class
a package need not mirror the directory, and import x as y resolves name clashes
fun main() with or without args: Array<String>
newlines end statements; semicolons are optional and only needed to pack a line
B. Basic types, numbers and strings
Byte, Short, Int, Long, Float, Double and the value range of each
an Int does not silently become a Long; you call toLong()
an integer literal is Int unless it exceeds the range or carries the L suffix
== calls equals and is null-safe; === compares references
=== on two boxed Ints is true inside the small-value cache and false outside it
NaN != NaN and -0.0 < 0.0 under static Double typing, but Comparable ordering says otherwise
UInt/ULong wrap on overflow and never implicitly convert to signed
a Char has no arithmetic conversion; use .code and Char(code)
triple quotes keep newlines verbatim; trimIndent() strips the common leading indent
Array<T> is fixed-size and mutable, and IntArray avoids boxing that Array<Int> pays for
C. Control flow
if yields a value, which is why Kotlin has no ternary operator
when used as an expression must cover every case or carry an else
with no argument each branch is an independent boolean condition
a branch can test in a range, is a type, or several comma-separated values
for walks anything providing iterator(); there is no C-style three-part loop
.. includes the end, ..< excludes it, and downTo/step build the reverse or strided progression
a loop@ label lets break@loop escape an outer loop from a nested one
a variable declared inside the do block is still visible in the while condition
D. Null safety
String cannot hold null, so the NPE class of bug is a compile error
T? is a separate type; passing it where T is expected does not compile
?. evaluates to null instead of throwing, and chains short-circuit at the first null
?: supplies a fallback, and its right side may be return or throw because those are Nothing
!! deliberately throws an NPE and exists to make the unchecked assumption visible
as? yields null rather than a ClassCastException on a type mismatch
x?.let { } runs only when non-null and gives the value a stable local name
an extension declared on T? is callable on null, which is how isNullOrEmpty() works
a value from unannotated Java is T!: no compile-time check, so the NPE lands at the boundary
defers a non-null var; reading it early throws UninitializedPropertyAccessException, and it is illegal on primitives and val
filterNotNull, orEmpty, and the difference between List<T?> and List<T>?
E. Type checks and smart casts
is and !is test the runtime type
after a successful is check the compiler uses the narrower type with no manual cast
no smart cast on a var property, a custom-getter property, or an open val, because the value could change between the check and the use
as throws ClassCastException when the type does not match
an is branch of a when narrows the subject inside that branch only
if (x !is T) return smart-casts x for the rest of the function
is List<String> will not compile; only is List<*> is checkable at runtime
F. Functions
defaults belong to the declaration, and an override may not restate them
naming an argument lets you skip earlier defaults and reorder for readability
one vararg per function, and an existing array is passed with the * spread operator
a parameter cannot be reassigned inside the function body
a function nested in another captures its enclosing locals
infix requires a member or extension with exactly one non-vararg, non-default parameter
tailrec compiles self-recursion to a loop only when the call is genuinely the last operation
Nothing marks a function that never returns normally, so the code after it is dead
a default argument usually replaces an overload set, and mixing both creates ambiguity
the type parameter goes before the function name, and may be given explicitly at the call site
G. Lambdas and higher-order functions
(Int) -> String is a first-class type, and a nullable one needs parentheses: ((Int) -> String)?
a lambda has no return; its final expression is its value
a single-parameter lambda names that parameter it unless you name it yourself
a lambda in the last argument position moves outside the parentheses
fun(x: Int): Int { ... } allows an explicit return type and an ordinary return
::top, bound obj::method, and ::ClassName as a constructor reference
a Kotlin lambda can read and reassign a captured var, unlike a Java lambda
a type A.() -> B makes this inside the lambda the receiver instead of a parameter
(k, v) -> destructures a pair, and _ discards a parameter you do not need
H. Inline functions and reified types
inlining copies the body and the lambda to the call site, removing the function-object allocation
a bare return inside an inline lambda returns from the *enclosing* function
marks one lambda parameter as a real object so it can be stored or passed on
keeps a lambda inlined but forbids non-local return, needed when it runs inside another lambda
only an inline function can reify a type parameter, which is what makes is T and T::class legal
inlining a large body multiplies bytecode, and the compiler warns when inlining buys nothing
a public inline function cannot touch non-public members, because the body is compiled into callers
I. Classes, constructors and initialization
declared in the class header, and val/var on a parameter also declares a property
init blocks and property initializers run in declaration order, interleaved
must delegate to the primary with this(...) when one exists
instantiation is a plain call, Foo(), so a constructor and a factory function look alike
a class cannot be subclassed unless declared open
restricting a constructor requires the explicit constructor keyword, as in private constructor()
a nested class holds no outer reference; inner does, and reaches it via this@Outer
calling an open member during construction sees the subclass's uninitialized state
every class implicitly extends Any, which supplies equals, hashCode and toString
J. Inheritance, interfaces and visibility
override is mandatory, and an overriding member stays open unless marked final
an abstract member is open implicitly and has no body
super.f() reaches the base, and super<Base>.f() disambiguates two supertypes
an interface may carry method bodies but no backing-field state
an interface property must be abstract or defined by an accessor, since it has no field
a fun interface with one abstract method accepts a lambda via SAM conversion
public is the default, and internal means visible within the compilation module
protected reaches subclasses only, unlike Java's package-visible default
a val may be overridden by a var, but never the reverse
K. Data, enum, sealed and value classes
data generates equals, hashCode, toString, componentN and copy
a property declared in the class body is excluded from equals and toString
copy() reuses the same nested objects, so a mutable field is shared with the original
destructuring binds by componentN order, so reordering properties silently breaks callers
needs a primary constructor with at least one val/var parameter, and cannot be abstract or open
enum constants are objects with name, ordinal, entries and valueOf
a constant can override a member with its own anonymous class body
the compiler knows every direct subclass, so a when over them needs no else
subclasses of a sealed type must live in the same module and package
a class can implement several sealed interfaces, which a sealed class hierarchy cannot express
@JvmInline value class wraps one property with no runtime allocation, but boxes when used as a supertype, generic argument or nullable
L. Objects, companions and delegation
object Foo is a lazily initialized, thread-safe singleton
an anonymous object is Kotlin's anonymous class, and may implement zero or several interfaces
members declared only on an anonymous object are reachable only where its type is inferred, not through a supertype
one per class, referenced by the class name, and its members are not JVM statics
operator fun invoke on the companion makes Foo(...) a factory call
data object supplies a readable toString for a singleton case in a sealed hierarchy
class A(b: B) : B by b generates every forwarding member for free
the delegate's own calls do not dispatch to the wrapper's overrides
M. Properties and delegated properties
the field identifier exists only inside a custom accessor, and only when one is used
a val with only a getter recomputes on every read and stores nothing
var x = 0; private set exposes a read-only property with a private writer
const val is a compile-time constant, allowed only at top level or in an object, for primitives and String
by lazy computes on first access and is thread-safe by default, at the cost of synchronization
Delegates.observable reports a change after it happens, vetoable can reject it first
by map reads a property's value out of a Map under the property name
a delegate supplies operator getValue/setValue, and provideDelegate customizes creation
N. Extensions and operator conventions
an extension is dispatched on the *declared* type, so it does not behave polymorphically
a member function always shadows an extension with the same signature
an extension is a static function with a receiver, so it sees only the public API
it must define an accessor; it cannot store state
an extension must be in scope by import to be callable, unlike a member
operators map to fixed function names: plus, minus, times, unaryMinus, rangeTo
<, >, <= and >= all route through one compareTo
get/set power a[i], and invoke makes an object callable as a()
+= prefers plusAssign on a mutable receiver, falls back to plus, and errors when both apply
contains powers in, and iterator makes a type usable in a for loop
O. Scope functions
takes it, returns the lambda's result, and is the standard null-guard
both use a this receiver and return the lambda's result; with is not an extension, so it cannot be chained on a nullable
this receiver, returns the receiver, so it is the object-configuration idiom
it parameter, returns the receiver, so it is the side-effect and logging idiom
choose by receiver (this vs it) and by return value (receiver vs lambda result)
return the receiver or null based on a predicate, which composes with ?:
nested scope functions shadow it and this, so name the parameter once you nest
P. Generics and variance
a generic declaration parameterizes over a type, and the argument is usually inferred at the call site
List<String> is not a MutableList<Any>, because a writable container would let you insert the wrong type
out T marks a producer, and T may then appear only in return positions
in T marks a consumer, and T may then appear only in parameter positions
Array<out T> restricts one usage rather than the whole declaration, the way a Java wildcard does
List<*> reads elements as Any? and refuses writes, because the real argument is unknown
<T : Comparable<T>>, and the where clause for more than one bound
an unbounded T already admits null; bound it by Any to exclude it
type arguments vanish at runtime, so two overloads differing only by them clash
passing a KClass or using an inline reified parameter recovers the erased type
unlike Java's covariant arrays, Array<String> is not an Array<Any>, which removes the store-check exception
Q. Collections and sequences
List only lacks mutating methods; the underlying object can still change through another reference
listOf, mutableListOf, setOf, mapOf, and buildList for incremental construction
mapOf and setOf preserve insertion order because they are backed by linked implementations
every map/filter step on a collection builds a new intermediate list
a sequence pushes each element through the whole chain and does nothing until a terminal operation
sequences win on long chains, large inputs and short-circuiting; they lose on small collections
reduce throws on an empty collection, fold starts from an initial value and can change the result type
flatMap flattens one level of nesting that map would leave behind
groupBy keeps every element per key, associateBy keeps only the last
sortedBy, compareBy/thenBy, and the stability that makes multi-key sorting work
the throwing family (first, single, max) versus the …OrNull family
mutating an object after using it as a set element or map key makes it unfindable
R. Coroutines: suspension and structured concurrency
a suspend function can pause without blocking a thread, and is callable only from a coroutine or another suspend function
many coroutines multiplex onto few threads, so they are cheap to create in the thousands
launch returns a Job and no value, async returns a Deferred that carries a result
bridges blocking and suspending worlds by blocking the caller's thread, so it belongs in main and tests, not in library code
a scope does not finish until every child coroutine finishes, so nothing leaks silently
one failed child cancels its siblings under coroutineScope, but not under supervisorScope
a child Job inherits from its parent, and cancelling the parent cancels the whole subtree
join waits for completion, await waits and returns the value or rethrows the failure
Thread.sleep or a blocking I/O call holds the dispatcher thread and starves other coroutines
async { }.await() on the next line runs sequentially; start both, then await both
S. Coroutine context, dispatchers and cancellation
a CoroutineContext is an indexed set of elements — Job, dispatcher, name, handler — combined with +
Default for CPU work, IO for blocking calls, Main for UI, Unconfined for the rare no-thread-confinement case
switches dispatcher for a block and returns its value; it is a sequential switch, not a new concurrent branch
a child inherits the parent's context but always gets its own Job
a coroutine keeps running until it hits a suspension point or checks isActive
a tight CPU loop must call one of them to become cancellable
catching a broad Exception swallows cancellation and breaks structured concurrency
suspending work in a finally needs withContext(NonCancellable) or it is cancelled immediately
launch reports a failure up to the CoroutineExceptionHandler, while async holds it until await
T. Flow and channels
a flow's builder runs once per collector, and nothing happens until collect
collection runs in the collector's coroutine and completes when the flow does
a flow builder may not withContext; changing the emission context is an error
flowOn changes the context of everything *upstream* of it, and nothing downstream
buffer decouples producer and consumer, conflate drops intermediate values, collectLatest cancels the in-flight collector
flow operators can call suspend functions inside them, which sequence operators cannot
hot, always holds a current value, conflates, and skips emissions equal to the previous one
hot, has no initial value, and its replay cache decides what a late subscriber sees
turn a cold flow hot in a scope, with a SharingStarted policy deciding when upstream runs
handle upstream failures with the catch operator; a try/catch wrapped around collect also catches the collector's own errors
hot, each element goes to exactly one receiver, capacity and BufferOverflow shape backpressure, and an unclosed channel hangs the receiver
U. Java interoperability
@Nullable/@NotNull on the Java side turn a platform type into a checked Kotlin type
Kotlin never forces a catch, and @Throws exists only so Java callers see the signature
makes a companion or object member a genuine JVM static for Java callers
generates the overload set that lets Java call a function with default arguments
@JvmName renames a facade or resolves an erasure clash, @JvmField exposes a property as a plain field
a Java getX/setX pair is visible from Kotlin as a property
top-level declarations in Util.kt land in a Java class named UtilKt
a Java single-abstract-method interface accepts a Kotlin lambda directly; a Kotlin interface needs fun interface
kotlin.collections.List compiles to java.util.List, so Java code can mutate a read-only Kotlin list
V. Exceptions, results, contracts and opt-in
there is no throws clause, so the compiler never forces you to handle one
val x = try { … } catch (e: E) { … } yields the value of whichever block ran
because throw is Nothing, it fits on the right of an elvis or in a when branch
a return inside finally discards a pending exception or return value
Result captures success or failure, and catching everything also captures CancellationException
contract { returns() implies (x is String) } lets a smart cast survive across a function call
@RequiresOptIn marks an unstable API, and callers either @OptIn or propagate the marker
W. Multiplatform basics
code in commonMain compiles for every declared target
a common expect declaration is satisfied by a matching actual per target, with identical signatures
a platform can satisfy an expect class by aliasing an existing platform type
an intermediate source set shares code across a subset of targets, such as all native ones
a platform source set sees commonMain, never the reverse
most of kotlin.* is available everywhere, while JVM-only APIs are not
JVM, Android, native, JS and Wasm targets each compile to their own artifact from the same sources
X. DSLs, annotations and reflection
nested receiver lambdas produce a tree-shaped, statically checked DSL
a builder takes block: Node.() -> Unit and applies it to a freshly constructed node
@DslMarker stops an inner block from silently resolving a member on the outer receiver
this@Outer names which receiver you mean when scopes nest
::class gives a KClass, and ::class.java crosses into Java reflection
@field:, @get:, @param: and @setparam: decide which generated element an annotation lands on
full reflection needs the separate kotlin-reflect artifact and is slow, so prefer a compile-time solution
Keentune is not affiliated with or endorsed by the organizations whose documentation informs these maps.
All about Kotlin practice
Also on your phone
All exam, test, and product names and trademarks are the property of their respective owners and are used here for identification and reference only. Keentune is independent study practice — not affiliated with, authorized, or endorsed by any of these organizations.
© 2026 SportaApp LLC