Kotlin language cheat sheet
The 20 Kotlin language interview answers in 30 seconds each, plus quick facts. Printable, free.
One page per topic: print it or save it as PDF from your browser's print menu. Practise these questions
Rules of thumb
- Kotlin compiles to bytecode, then DEX; ART runs it, not a JVM.
- !! throws NullPointerException; prefer ?. ?: and requireNotNull.
- val is a read-only reference, not an immutable object.
- Sealed types plus exhaustive when, without else.
- Extensions are resolved by declared type.
- Use == for values, === only for identity.
How Kotlin code runs on Android
The Kotlin compiler turns source into JVM bytecode (.class files). The Android build then converts that bytecode to DEX with D8, shrinks and optimises it with R8, and packages it into the APK or bundle. On the device, the Android Runtime (ART) runs the DEX code using a mix of interpretation, JIT and ahead-of-time compilation. There's no Java Virtual Machine on the phone.
Null safety and !!
Types are non-null by default; String? can hold null and the compiler forces you to handle it. Use ?. for safe calls, ?: for a default, let to run code only when non-null, and smart casts after a null check. !! tells the compiler 'trust me' and throws a NullPointerException if the value is null.
val, var and const val
var can be reassigned. val can't be reassigned after initialisation, but the object it points to can still change, like a MutableList. const val is a compile-time constant: a primitive or String at top level or in an object, inlined into every use. Prefer val, and immutable types, unless you really need to reassign.
Data classes
A data class generates equals, hashCode, toString, copy and componentN functions from the properties in its primary constructor. That makes it ideal for value-like models and UI state. Properties declared in the body are ignored by those functions, and copy is shallow, so nested mutable objects are shared.
Sealed classes and exhaustive when
A sealed class or interface has a fixed set of subtypes known at compile time, all in the same module and package. Because the compiler knows every case, a when expression over it must handle each one or have else. Adding a new case then breaks every when that forgot it, which is exactly what you want for UI state or results.
== vs ===
== checks structural equality by calling equals (and handles null safely). === checks referential identity: whether both sides are the same object. For data classes, two objects with the same properties are == but not ===. For basic types like Int, use ==; identity on boxed numbers depends on caching and isn't meaningful.
let, run, with, apply, also
They differ in two ways: how the object is referenced inside (it for let/also, this for run/with/apply) and what they return (the lambda result for let/run/with, the object itself for apply/also). Use apply to configure an object, also for side effects like logging, let for null checks and transforms, and run or with to compute a result from an object.
Extension functions
An extension function adds a function to a type without changing it: fun String.initials(): String. It compiles to a static function that takes the receiver as its first parameter. So extensions are resolved statically by the declared type, not the runtime type, and a member function with the same signature always wins.
lateinit vs by lazy
lateinit var is for a non-null property that is set later by something else, like dependency injection or a test setup, before it's read. Reading it early throws UninitializedPropertyAccessException. val x by lazy { } computes the value on first access and caches it, thread-safely by default. Use lazy for expensive values you can compute yourself.
inline functions and reified
An inline function's body, and the lambdas passed to it, are copied into each call site. That removes the cost of allocating lambda objects and allows return from inside the lambda to return from the caller. Because the code is copied, the compiler knows the actual type at each call, which is what makes reified T possible, so you can write is T or T::class.
inline fun <reified T> List<Any>.firstOfType(): T? =
firstOrNull { it is T } as T?
fun main() {
println(listOf(1, "two", 3.0).firstOfType<String>())
}Lambdas, function types and fun interfaces
A function type like (String) -> Int describes a function's parameters and result, so functions can be passed around as values. A lambda { s: String -> s.length } is a value of that type. A fun interface is an interface with one abstract method that can be created from a lambda (SAM conversion), useful for Java interop and named callbacks.
object, companion object and singletons
An object declaration defines a class and its single instance at once: a thread-safe, lazily created singleton. A companion object is an object tied to a class, used for factory functions and constants called on the class name. An object expression creates an anonymous object on the spot, like Java's anonymous classes.
class Money private constructor(val cents: Long) {
companion object {
fun ofEuros(euros: Int) = Money(euros * 100L)
}
}Generics: in, out and star projection
out T means the type only produces T (covariant), so a Producer<String> can be used as a Producer<Any>. in T means it only consumes T (contravariant), so a Consumer<Any> can be used as a Consumer<String>. Kotlin's List<out E> is read-only, so List<String> is a subtype of List<Any>, but MutableList<String> is not a MutableList<Any>.
Delegation with by
Class delegation (class Logged(private val inner: Repo) : Repo by inner) generates every interface method to forward to inner, so you override only what you change. That's composition without boilerplate. Property delegation (val x by lazy { }, by mutableStateOf(), by viewModels()) hands a property's get and set to a delegate object.
Smart casts and their limits
After a check like if (x is String) or if (x != null), the compiler treats x as the narrower type without an explicit cast. It only does this when it can prove the value can't change between the check and the use: local vals, local vars not captured by a lambda that modifies them, and val properties in the same module without custom getters. Mutable properties can't be smart-cast.
Value classes
@JvmInline value class UserId(val raw: String) wraps a single value in its own type, so you can't pass an OrderId where a UserId is expected. At runtime the compiler usually uses the underlying value directly, so there's no extra object. It's type safety for IDs, units and validated strings at almost no cost.
Collections vs sequences
List operations like map and filter run eagerly: each step builds a full intermediate list. A Sequence runs lazily, element by element, through the whole chain, and stops as soon as a terminal operation like first or take has what it needs. Use sequences for long chains on large collections or when you stop early; lists are faster for small ones.
Kotlin 2 and the K2 compiler
Kotlin 2.0 made K2 the default compiler. It has a new frontend that is much faster to compile, unified across JVM, Native, JS and Wasm, and smarter at smart casts. The Compose compiler moved into the Kotlin repository and ships with each Kotlin release as a Gradle plugin. Most code compiles unchanged, but some edge-case errors and warnings changed.
@JvmStatic, @JvmField and @JvmOverloads
They shape how Kotlin code looks to Java callers. @JvmStatic on a companion or object member generates a real static method. @JvmField exposes a property as a public field with no getter or setter. @JvmOverloads generates Java overloads for a function or constructor with default arguments, which custom Views need.
class BadgeView @JvmOverloads constructor(
context: Context,
attrs: AttributeSet? = null,
defStyleAttr: Int = 0
) : View(context, attrs, defStyleAttr)Visibility: internal and friends
public (the default) is visible everywhere, private inside the file or class, protected in the class and subclasses, and internal inside the same Gradle module. In a modular app, internal hides implementation classes so other modules can only use the public API, which keeps module boundaries real and builds faster.
Quick facts
- Elvis operator
- a ?: b returns a if it isn't null, otherwise b; b can be throw or return.
- Kotlin typealias
- A typealias gives a type another name; it doesn't create a new type.
- data object
- data object gives a singleton a readable toString, plus equals and hashCode.
- Trailing lambdas
- If a function's last parameter is a function, the lambda can go outside the parentheses.
- when guard conditions
- Kotlin 2.2 made guard conditions in when stable: is Shape.Circle if radius > 0 ->.
- Range operators
- 1..5 includes 5; 1..<5 (rangeUntil) excludes it.