Back
Keentune
Kotlin curriculum 26 chapters
·
226 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 practicing and let the questions find your level.
Start practicing Kotlin
New here? Read the Kotlin guide A free 18-minute primer — the mental model, the mistakes beginners make, and what to practice 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.
Practice this section →
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 Int s 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.
Practice this section →
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.
Practice this section →
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>? .
Practice this section →
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.
Practice this section →
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.
Practice this section →
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.
Practice this section →
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.
Practice this section →
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 .
Practice this section →
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.
Practice this section →
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.
Practice this section →
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.
Practice this section →
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.
Practice this section →
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.
Practice this section →
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.
Practice this section →
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.
Practice this section →
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.
Practice this section →
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 canceling 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.
Practice this section →
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 canceled immediately.
•
launch reports a failure up to the CoroutineExceptionHandler , while async holds it until await .
Practice this section →
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.
Practice this section →
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.
Practice this section →
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.
Practice this section →
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.
Practice this section →
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.
Practice this section →
Y. Shared mutable state in coroutines •
Many coroutines on a multi-threaded dispatcher incrementing one var lose updates, because ++ is a read, an add and a write.
•
@Volatile guarantees VISIBILITY of the latest value and nothing about atomicity, so a volatile counter still loses increments.
•
AtomicInteger.incrementAndGet and friends make the read-modify-write a single operation, which is the right answer for a counter.
•
Pinning the state to one single-threaded dispatcher removes the race; confining each operation is fine-grained and slow, confining the whole block is coarse-grained and fast.
•
Mutex.withLock SUSPENDS the coroutine while it waits instead of blocking the thread, which is what separates it from synchronized .
Practice this section →
Z. Selecting among suspending alternatives •
select awaits several suspending alternatives together and takes the first that becomes available; it is an EXPRESSION, so it returns the value its chosen clause produced. It is marked experimental in kotlinx.coroutines .
•
When more than one clause is ready at the same instant the FIRST one written wins, which makes clause order a scheduling decision rather than a style choice.
•
onReceive throws once its channel is closed, while onReceiveCatching turns the close into a value and is selected immediately on a channel that is already closed.
•
onSend chooses whichever channel can accept right now, and combined with the first-clause bias that is how a producer spills to a side channel when its primary consumer cannot keep up.
•
onAwait selects among Deferred values rather than channels, so the fastest of several concurrent async results can be taken without canceling the rest.
Practice this section →
Keentune is not affiliated with or endorsed by the organizations whose documentation informs these maps.
Start practicing Kotlin
All about Kotlin practice
Browse all free study guides
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