🐦

Top 100 Kotlin Interview Questions

Top 100 Kotlin interview questions covering variables, null safety, OOP, collections, coroutines, Flow, and advanced language features.

100 Questions
Filter:

Kotlin is a modern, statically typed programming language developed by JetBrains that runs on the JVM and is fully interoperable with Java. It was first released in 2011 and became Google's officially recommended language for Android development in 2017. Kotlin is designed to be concise, expressive, and safer than Java, eliminating common pitfalls like NullPointerExceptions at the language level. It can also compile to JavaScript (Kotlin/JS) and native binaries (Kotlin/Native), making it a true multiplatform language.

Beginner

In Kotlin, val declares an immutable (read-only) reference — once assigned, the reference cannot be changed. It is equivalent to Java's final. var declares a mutable variable that can be reassigned any number of times. Best practice is to always prefer val by default and only use var when mutability is truly needed. Note that val means the reference is immutable, not the object it points to — a val list can still have its contents modified.

Beginner

By default, all types in Kotlin are non-nullable — you cannot assign null to a regular String. To allow null, append a ? to the type: var name: String? = null. The Kotlin compiler enforces null safety at compile time — you cannot call methods on a nullable variable without first handling the null case, either with a null check, the safe call operator ?., the Elvis operator ?:, or the non-null assertion operator !!. This design eliminates most NullPointerExceptions at compile time.

Beginner

The safe call operator ?. allows you to call a method or access a property on a nullable object without risking a NullPointerException. If the object is null, the expression returns null instead of throwing an exception. For example, name?.length returns the string length if name is not null, or null if it is. Safe calls can be chained: user?.address?.city — if any value in the chain is null, the entire expression returns null. This is one of Kotlin's most used null-safety features.

Beginner

The Elvis operator ?: provides a default value when the left-hand expression is null. The expression name?.length ?: 0 returns the length of name if it is not null, or 0 if it is null. The name comes from the Elvis emoji (the ?: emoticon looks like Elvis's hair and eye). You can also throw an exception as the right-hand side: val name = user?.name ?: throw IllegalArgumentException("User must have a name"). This is extremely useful for providing fallback values concisely.

Beginner

The non-null assertion operator !! converts a nullable type to a non-nullable type, explicitly telling the compiler "I know this is not null." If the value is actually null at runtime, a KotlinNullPointerException is thrown immediately. Use !! only when you are absolutely certain a value cannot be null — for example, immediately after a null check that the compiler cannot track. In general, avoid !! in production code and prefer ?. or ?:, as !! can introduce the very null crashes Kotlin is designed to prevent.

Beginner

String interpolation in Kotlin allows you to embed variables and expressions directly inside string literals using the dollar sign. For a simple variable, use "Hello, $name". For more complex expressions, wrap them in curly braces: "Hello, ${user.name.uppercase()}". This is much cleaner and more readable than Java's string concatenation with +. Kotlin string templates are evaluated at runtime and support any valid Kotlin expression inside the curly braces, including function calls and arithmetic.

Beginner

Functions in Kotlin are declared using the fun keyword, followed by the function name, parameters, return type, and body. Example: fun add(a: Int, b: Int): Int { return a + b }. For single-expression functions, you can use the shorthand expression syntax: fun add(a: Int, b: Int): Int = a + b — the return type can even be inferred: fun add(a: Int, b: Int) = a + b. Functions that return nothing implicitly return Unit, which can be omitted from the signature.

Beginner

Unit is Kotlin's equivalent of Java's void, but with an important difference — Unit is an actual type with exactly one value (also called Unit). A function that does not return a meaningful value has its return type set to Unit, though you can omit it from the signature. Because Unit is a real type, it can be used as a generic type argument — for example, you can have a Future<Unit> in coroutines. This makes the type system consistent, unlike Java where void cannot be used as a generic type.

Beginner

A data class is a special class designed to hold data. Kotlin automatically generates several standard methods based on the properties declared in the primary constructor: equals() and hashCode() for value comparison, toString() for readable output, copy() for creating modified copies, and componentN() functions for destructuring. Example: data class User(val name: String, val age: Int). The copy() function is particularly useful: val updated = user.copy(age = 26) creates a new User with only the age changed, leaving all other fields intact.

Beginner

The object keyword creates a singleton — a class with exactly one instance that is created lazily on first access in a thread-safe manner. You declare it like a class but without a constructor: object DatabaseManager { fun connect() {} }. Access it directly by name: DatabaseManager.connect(). This replaces the verbose singleton pattern from Java. Object declarations can extend classes and implement interfaces. They are perfect for utility classes, managers, and anything that should have only one instance in the application.

Beginner

A companion object is a singleton object declared inside a class. Its members can be accessed using the class name, similar to static members in Java. class MyClass { companion object { fun create() = MyClass() } } — called as MyClass.create(). Companion objects can implement interfaces and have a name (optional). Unlike Java static members, companion object methods are real instance methods of the singleton. Use the @JvmStatic annotation on companion members to generate actual static methods for Java interoperability.

Beginner

In Kotlin, == checks structural equality — it calls the equals() method. For data classes, this compares all declared properties. For example, two different User objects with the same properties would be equal with ==. === checks referential equality — it checks if two references point to the exact same object in memory. This is equivalent to Java's ==. For primitive types like Int, Kotlin compiles == to == in bytecode, but for object types it uses equals().

Beginner

The when expression is Kotlin's powerful replacement for Java's switch statement. It is far more expressive — each branch can match a specific value, a range (in 1..10), a type (is String), or an arbitrary condition. When used as an expression (its result is assigned or returned), all cases must be covered and an else branch is required unless all possible values are covered (e.g., with a sealed class or enum). Unlike switch, when branches do not fall through, so you never need break statements.

Beginner

A sealed class is an abstract class whose subclasses must all be defined in the same package and module (in Kotlin 1.5+, the same file was the original requirement). This gives the compiler complete knowledge of all possible subtypes. The key benefit is exhaustive when expressions — when you match over a sealed class, the compiler knows all subclasses and will warn (or error) if you forget to handle one, without needing an else branch. Sealed classes are the idiomatic way to model restricted hierarchies like network results, UI states, or navigation events.

Beginner

In Kotlin, all classes and methods are final by default — they cannot be subclassed or overridden. The open keyword explicitly marks a class or method as inheritable/overridable. You write open class Animal to allow subclassing: class Dog : Animal(). Similarly, open fun sound() allows override fun sound() in a subclass. This design decision encourages composition over inheritance and prevents accidental subclassing of classes not designed for it, which is a common source of bugs in Java.

Beginner

The primary constructor is declared directly in the class header: class Person(val name: String, val age: Int). Properties declared with val or var in the constructor are automatically created — no need for field declarations and assignments in the body. Initialization logic goes in init { } blocks, which run as part of the primary constructor. If the constructor needs a visibility modifier or an annotation, add the constructor keyword: class Person private constructor(val name: String).

Beginner

The init block contains initialization code that runs as part of the primary constructor when an object is created. It executes in the order it appears in the class body, interleaved with property initializers. Multiple init blocks are allowed. Example: init { require(age >= 0) { "Age cannot be negative" } }. This is the place for validation logic and setup that cannot be expressed as a single property initializer expression. Secondary constructors must delegate to the primary constructor, so init blocks always run regardless of which constructor is used.

Beginner

A Kotlin interface defines a contract of methods and properties that implementing classes must provide. Unlike abstract classes, interfaces cannot hold state (no backing fields for properties) and cannot have constructors. However, they can provide default method implementations, which classes can optionally override. A class can implement multiple interfaces. Interfaces with default implementations are compiled to Java 8 default interface methods on the JVM. Use interfaces when you want to define a capability that multiple unrelated classes can share.

Beginner

A lambda expression is an anonymous function that can be passed as a value. The syntax is { parameters -> body }. Example: val double = { x: Int -> x * 2 }. If the lambda has a single parameter, you can omit it and use the implicit it name: list.filter { it > 5 }. If the lambda is the last argument of a function, it can be placed outside the parentheses (trailing lambda syntax). Lambdas are used extensively with higher-order functions for collection processing, event handling, and callbacks.

Beginner

A higher-order function is a function that takes another function as a parameter or returns a function. The parameter type is a function type, like (Int, Int) -> Int. Example: fun operate(a: Int, b: Int, op: (Int, Int) -> Int) = op(a, b), called as operate(3, 4) { x, y -> x + y }. Kotlin's entire collections API is built on higher-order functions — filter, map, forEach, reduce all take lambdas. This enables a functional programming style that is more expressive and composable than traditional loops.

Beginner

An extension function adds a new method to an existing class without modifying the class itself, inheriting from it, or using decorators. The syntax is fun ClassName.newMethod(): ReturnType { }. Inside the function, this refers to the object the function is called on. Example: fun String.isPalindrome() = this == this.reversed() — now "racecar".isPalindrome() returns true. Extensions are resolved statically (at compile time), not polymorphically. They are a key Kotlin feature for creating readable, fluent APIs without inheritance.

Beginner

The let scope function executes a block of code with the object as its argument (accessed as it inside the block) and returns the result of the block. It is most commonly used for null-safe operations: name?.let { println(it.uppercase()) } — the block only runs if name is not null. It is also useful for chaining transformations or when you want to confine a variable's scope to a specific block. Unlike apply and also, let returns the block's result, not the original object.

Beginner

The apply scope function executes a block where this refers to the object, and then returns the object itself (not the result of the block). This makes it ideal for object initialization and configuration — you can set multiple properties without repeating the variable name: val button = Button().apply { text = "OK"; isEnabled = true; setOnClickListener { ... } }. It returns the configured object, making it chainable. The apply block can access all members of the object directly without the it. prefix.

Beginner

A range in Kotlin represents an interval between two values, created with the .. operator: 1..10 creates an inclusive range from 1 to 10. Ranges work with any Comparable type, including characters and strings. For an exclusive end, use until: 0 until 10 (0 to 9). For descending ranges, use downTo: 10 downTo 1. You can also specify a step: 1..10 step 2 gives 1, 3, 5, 7, 9. Ranges integrate seamlessly with for loops and the in operator for membership checks.

Beginner

Default parameters allow function parameters to have pre-defined values that are used when the caller omits that argument. Example: fun greet(name: String = "World") = "Hello, $name!" can be called as greet() or greet("Kotlin"). Default parameters dramatically reduce the need for overloaded functions — a single function with defaults replaces many Java overloads. They work well with named arguments: createUser(name = "Alice", admin = true) clearly documents intent and avoids mistakes with boolean parameters.

Beginner

Named arguments allow you to specify the parameter name when calling a function, which lets you pass arguments in any order and makes the code self-documenting. Example: fun createUser(name: String, age: Int, admin: Boolean = false) can be called as createUser(age = 25, name = "Alice"). Named arguments are particularly valuable for functions with multiple parameters of the same type or boolean flags, where positional arguments make the call site confusing. They also let you skip any subset of default parameters.

Beginner

Any is the root of the Kotlin class hierarchy — every non-nullable Kotlin class is a subtype of Any. It is equivalent to Java's Object. Any provides three methods that all classes inherit: equals(), hashCode(), and toString(). For nullable types, Any? is the supertype of all types including nullable ones. Unlike Java, Kotlin's primitive types (Int, Boolean, Double) are also subtypes of Any — they are represented as JVM primitives for performance but behave as objects in Kotlin's type system.

Beginner

Nothing is a type with no instances — a function that returns Nothing never returns normally. It is used for functions that always throw an exception or loop forever: fun fail(msg: String): Nothing = throw IllegalStateException(msg). Nothing is a subtype of every type, which means it can be used anywhere any type is expected. This is why throw expressions are valid in places like the Elvis operator: val name = user?.name ?: throw RuntimeException(). The TODO() function also returns Nothing, signaling unimplemented code.

Beginner

A destructuring declaration lets you unpack an object into multiple variables in a single statement. val (name, age) = user calls user.component1() for name and user.component2() for age. Data classes automatically provide componentN() functions for each property. You can also destructure in for loops: for ((key, value) in map). To skip a component you don't need, use an underscore: val (name, _, email) = user. Lambdas also support destructuring in their parameters.

Beginner

Kotlin has four visibility modifiers. public (default) — visible everywhere. private — visible only within the same class or file. protected — visible within the class and its subclasses (not applicable to top-level declarations). internal — visible within the same compilation module (a Gradle module, Maven artifact, etc.) but not from outside it. The internal modifier is unique to Kotlin and is extremely useful for library development — you can hide implementation details from library consumers while sharing them across multiple files within the library.

Beginner

The is operator checks if an object is an instance of a given type at runtime, like Java's instanceof. But Kotlin adds smart casting — after a successful is check, the compiler automatically casts the variable to the checked type within the conditional scope, without requiring an explicit cast. Example: if (obj is String) { println(obj.length) } — inside the if block, obj is automatically treated as a String. The !is operator checks that an object is NOT of the given type.

Beginner

An enum class defines a type with a fixed set of named constants, each of which is a singleton instance of the enum class. Example: enum class Direction { NORTH, SOUTH, EAST, WEST }. Unlike Java, Kotlin enums can have properties and methods: enum class Color(val hex: String) { RED("#FF0000"), GREEN("#00FF00") }. When used in a when expression, Kotlin can verify exhaustiveness — if you cover all enum values, you do not need an else branch. Prefer sealed classes when you need instances with different properties per variant.

Beginner

The also scope function executes a block with the object as its argument (accessed as it), performs a side effect, and returns the original object — not the block's result. It is similar to apply but uses it instead of this, making it clearer when you want to read the object's properties rather than configure them. Common use: val user = createUser().also { log("Created: ${it.name}") }. The word "also" reads naturally: "do this, and also log it." It is ideal for adding debugging, logging, or validation without interrupting a method chain.

Beginner

The run scope function executes a block where this is the object and returns the result of the block — it combines the object access of apply with the return value of let. Example: val result = user.run { "${name} is ${age} years old" }. It is also usable as a non-extension function: val result = run { val x = compute(); x * x }, which is useful for creating a local scope for computation without creating a dedicated function. Use run when you need both object access and want to return a transformed value.

Beginner

The with function takes an object and a block, makes the object available as this inside the block, and returns the block's result. Unlike the other scope functions, with is not an extension function — it is called as a regular function: with(user) { println(name); println(age) }. It is best used when you want to call multiple methods on a non-nullable object without repeating its name. The main difference from run is syntactic — with is preferred when the object is passed as an argument rather than chained.

Beginner

Kotlin distinguishes between read-only and mutable collections. Use listOf(1, 2, 3) to create an immutable list — you cannot add, remove, or update elements. Use mutableListOf(1, 2, 3) to create a mutable list that you can modify. Both are backed by Java's ArrayList. For an empty list, use emptyList() or listOf(). Kotlin also has arrayListOf() for an explicit ArrayList. Always prefer the read-only listOf() by default and only use mutable when necessary — this makes code safer and easier to reason about.

Beginner

A Map stores key-value pairs where each key is unique. Create an immutable map with mapOf("a" to 1, "b" to 2) (using the to infix function to create pairs) or a mutable one with mutableMapOf(). Access values with map["key"] (returns nullable) or map.getValue("key") (throws if key is missing). Kotlin maps support forEach { (k, v) -> } destructuring syntax for clean iteration. For Java interoperability, hashMapOf() and linkedMapOf() create explicit HashMap and LinkedHashMap instances.

Beginner

When a lambda expression has exactly one parameter, you can omit the parameter declaration and its arrow, and refer to the parameter using the implicit name it. For example, list.filter { it > 5 } is shorthand for list.filter { element -> element > 5 }. The it convention keeps short lambdas concise. For nested lambdas or when clarity matters, always declare an explicit parameter name to avoid ambiguity about which object it refers to. If a lambda has zero or two or more parameters, it is not available.

Beginner

Kotlin Coroutines are a concurrency design pattern that allows you to write asynchronous, non-blocking code in a sequential, easy-to-read style. Instead of callbacks or reactive streams, you write code that looks synchronous but runs asynchronously. Coroutines are lightweight — you can run thousands of them on a single thread without the overhead of Java threads, because they do not block threads; they suspend and resume. They are integrated into the Kotlin standard library and have first-class support in Android with viewModelScope and lifecycleScope.

Intermediate

A suspend function is a function that can pause its execution without blocking the underlying thread and resume later when its result is ready. It is marked with the suspend modifier and can only be called from another suspend function or a coroutine builder. Under the hood, the Kotlin compiler transforms suspend functions into state machines using continuation-passing style. This transformation allows non-blocking I/O — when a suspend function is waiting for a network response, the thread is free to do other work. This is fundamentally different from blocking I/O where the thread is stuck waiting.

Intermediate

Both are coroutine builders, but they serve different purposes. launch starts a coroutine for fire-and-forget work — it returns a Job but no result. Use it when you want to perform a side effect (save data, show a notification). async starts a coroutine that computes a result — it returns a Deferred<T>. Call await() on the Deferred to get the result, which suspends the caller until the async block completes. Use async for parallel decomposition: launch multiple async blocks and then await all results.

Intermediate

Dispatchers determine which thread or thread pool a coroutine runs on. Dispatchers.Main runs on the main/UI thread — use for UI updates. Dispatchers.IO is optimized for I/O-blocking work (network, file, database) and uses a shared pool of up to 64 threads. Dispatchers.Default is for CPU-intensive work (sorting, parsing) and uses a pool sized to the number of CPU cores. Dispatchers.Unconfined runs in whatever thread first calls it — generally avoid this in production. Switch dispatchers with withContext().

Intermediate

Kotlin Flow is a cold asynchronous data stream that emits multiple values sequentially, as opposed to a suspend function which returns only a single value. "Cold" means a flow does not produce values until a collector subscribes to it, and each collector gets its own independent execution. Flow integrates naturally with coroutines and supports reactive operators like map, filter, transform, and combine. You collect a flow inside a coroutine with flow.collect { value -> }. For hot streams that share values among collectors, use StateFlow or SharedFlow.

Intermediate

StateFlow is a hot flow that holds the current state value and replays it to new collectors. It is a modern, coroutine-friendly replacement for LiveData in Android. Unlike a regular cold Flow, StateFlow is always active and always has a value. New collectors immediately receive the current value on subscription. Update the state by assigning to a MutableStateFlow's value property. In ViewModels, expose it as the immutable interface: private val _uiState = MutableStateFlow(initialState); val uiState: StateFlow<State> = _uiState.asStateFlow().

Intermediate

SharedFlow is a hot flow that broadcasts emissions to all active collectors simultaneously. Unlike StateFlow, it does not hold a current value and new collectors do not receive past emissions by default (configurable via replay parameter). It is ideal for one-time events like navigation commands, error messages, or analytics events that should not be re-delivered when the UI resubscribes. Configure the extraBufferCapacity and onBufferOverflow to handle backpressure when collectors are slow. In a ViewModel, expose it as SharedFlow and emit via the MutableSharedFlow.

Intermediate

by lazy is a property delegate that defers initialization of a property until it is first accessed, then caches the result for all subsequent accesses. The lambda you pass is executed only once: val database: Database by lazy { Database.initialize() }. By default, lazy is thread-safe (SYNCHRONIZED mode — uses a lock). For single-threaded code, you can use lazy(LazyThreadSafetyMode.NONE) for better performance. This pattern is especially useful for expensive initializations that might not be needed in all code paths, like database connections or view references.

Intermediate

Property delegation allows you to delegate the getter and setter logic of a property to another object using the by keyword. The delegate object must implement getValue() (and setValue() for mutable properties). Kotlin's standard library provides built-in delegates: lazy (lazy initialization), Delegates.observable() (callback on change), Delegates.vetoable() (conditionally reject changes), and map (store properties in a map). You can create custom delegates by implementing the ReadOnlyProperty or ReadWriteProperty interfaces, enabling reusable property behavior like validation or logging.

Intermediate

An inline function is marked with the inline modifier and has its entire body — including any passed lambda bodies — copied (inlined) to each call site at compile time. This eliminates the overhead of creating a lambda object and the virtual call, which is significant in performance-critical code called in tight loops. Beyond performance, inlining enables two features that are otherwise impossible: non-local returns (returning from the enclosing function inside a lambda) and reified type parameters (accessing generic type information at runtime). All of Kotlin's standard collection functions like filter and map are inline.

Intermediate

Due to JVM type erasure, generic type information is lost at runtime — you cannot write obj is T in a generic function. Kotlin's reified keyword, used only with inline functions, preserves the generic type at runtime by inlining the type checks at each call site. inline fun <reified T> isType(obj: Any) = obj is T — the compiler replaces each call with the actual type check (obj is String, obj is Int, etc.). This enables type-safe JSON parsing, dependency injection lookups, and Gson/Retrofit type adapters without passing Class<T> parameters explicitly.

Intermediate

The filter function returns a new list containing only the elements that satisfy a given predicate (a lambda returning Boolean). val adults = users.filter { it.age >= 18 }. The original collection is not modified. If no elements match, an empty list is returned (never null). Related functions: filterNot { } keeps elements that do NOT match the predicate, filterIsInstance<T>() filters by type and smart-casts the results, and filterNotNull() removes null elements from a nullable collection. All these functions create intermediate lists — use Sequences for chaining multiple operations efficiently.

Intermediate

The map function transforms each element of a collection using a provided function and returns a new list of the same size. val names = users.map { it.name } returns a List<String> from a List<User>. Every element is transformed — the result always has the same number of elements as the original. Related variants: mapNotNull { } transforms and filters out null results in one pass, mapIndexed { index, element -> } provides access to the element's position, and flatMap { } for when the transform returns a list that should be flattened.

Intermediate

flatMap transforms each element into a collection and then flattens all resulting collections into a single list. It is equivalent to calling .map { }.flatten() in one step. Example: val allTags = posts.flatMap { it.tags } — if each post has a list of tags, flatMap gives you one flat list of all tags from all posts. This is one of the most useful collection functions for working with nested structures. Use it whenever your map function returns a List and you want a flat result instead of a list of lists.

Intermediate

A Sequence is a lazily-evaluated collection that processes elements one at a time rather than creating intermediate collections at each step. When you chain operations on a regular List (filter then map), each operation creates a new intermediate list. With Sequences, all operations are applied to each element in one pass. Convert any collection to a sequence with asSequence(). Sequences are more efficient for large collections with multiple chained transformations, or when you only need the first few results. However, for small collections, the overhead of lazy evaluation can make Sequences slower.

Intermediate

groupBy groups the elements of a collection by a key derived from each element, returning a Map<K, List<V>>. Example: val byDepartment = employees.groupBy { it.department } returns a map where each key is a department name and each value is the list of employees in that department. For getting only the first element per key, use associateBy { } instead. The result map is ordered by the first occurrence of each key. For multi-level grouping, chain groupBy calls on the resulting lists.

Intermediate

fold accumulates a value starting from an initial value and applying an operation to each element from left to right. val sum = numbers.fold(0) { acc, n -> acc + n }. The accumulator (acc) starts at the initial value and is updated by each element. Unlike reduce, fold always has an explicit initial value, so it is safe for empty collections. Use foldRight to process elements from right to left. Fold is very powerful for building complex results like grouped maps or custom data structures from a collection.

Intermediate

Structured concurrency is the principle that coroutines are scoped to a specific lifetime. Every coroutine belongs to a CoroutineScope, and a parent scope waits for all its child coroutines to complete before it itself completes. If a parent scope is cancelled, all child coroutines are automatically cancelled. If a child fails with an uncaught exception, it propagates to the parent and cancels siblings. This prevents coroutine leaks (coroutines that keep running after their associated UI is destroyed) and makes async code behave predictably. Android's viewModelScope and lifecycleScope implement this principle.

Intermediate

withContext(dispatcher) is a suspend function that switches the coroutine to a different dispatcher for the duration of the block, then switches back. It is the idiomatic way to perform off-main-thread work: val result = withContext(Dispatchers.IO) { api.fetchData() }. Unlike async { }.await(), withContext does not create a new coroutine — it reuses the current coroutine's Job, making it more efficient for a single sequential task. Use it whenever you need to change the execution context for a specific operation, like moving I/O work off the main thread and back.

Intermediate

In a regular coroutine scope with a standard Job, if any child coroutine fails, the exception cancels all other children and the parent. A SupervisorJob changes this behavior — each child coroutine is independent, so a failure in one child does not affect siblings. This is ideal for a ViewModel that makes multiple independent network requests in parallel — if one request fails, the others should continue. Android's viewModelScope uses a SupervisorJob internally. For finer control, use supervisorScope { } to create a temporary supervisor scope within your coroutine.

Intermediate

By default, companion object methods are instance methods of the companion singleton, not static methods in the JVM bytecode. This means Java code must call them as MyClass.Companion.method(). Annotating a companion object method with @JvmStatic generates an additional static method in the enclosing class's bytecode, allowing Java callers to use the natural MyClass.method() syntax. Similarly, @JvmField exposes a companion property as a real static field. These annotations are important for maintaining clean Java interoperability in mixed Kotlin-Java codebases or when publishing Kotlin libraries for Java consumption.

Intermediate

Operator overloading lets you provide custom implementations for predefined operators for your own types. You mark a function with the operator modifier and use a specific function name corresponding to the operator: plus for +, minus for -, times for *, get/set for [], invoke for (), compareTo for comparison, etc. Example: operator fun Point.plus(other: Point) = Point(x + other.x, y + other.y) — now p1 + p2 works. Operator overloading should only be used when the operator has a clear, intuitive meaning for the type.

Intermediate

Delegates.observable() is a built-in property delegate that calls a handler lambda every time the property value changes, after the change has been applied. var name: String by Delegates.observable("initial") { prop, old, new -> println("Changed from $old to $new") }. The handler receives the property metadata, old value, and new value. It is useful for reacting to property changes for debugging, UI updates, or triggering side effects. For cases where you want to prevent a change, use Delegates.vetoable() instead, which allows the handler to reject the new value.

Intermediate

A type alias provides an alternative name for an existing type without creating a new type. Declared with typealias: typealias UserMap = Map<String, User>. The alias and the original type are fully interchangeable at compile time — the compiler replaces the alias with the actual type. Type aliases are most useful for shortening complex generic types, naming function types for clarity (typealias Callback = (Result<Data>) -> Unit), and giving meaningful names to platform types. Unlike value classes (inline classes), type aliases have zero runtime overhead but also provide no type safety between the alias and the original.

Intermediate

Java does not support Kotlin's default parameter values, so a Kotlin function with 3 default parameters appears as a single overload to Java. The @JvmOverloads annotation instructs the compiler to generate additional Java-visible overloaded methods for each combination of default parameters, from the required parameters up to the full signature. This makes the function much more ergonomic to call from Java. It is commonly applied to Android View custom view constructors and any Kotlin functions with defaults that Java code needs to call.

Intermediate

reduce is similar to fold but uses the first element of the collection as the initial accumulator value instead of requiring you to provide one. Example: val sum = numbers.reduce { acc, n -> acc + n }. Because the first element is used as the starting value, reduce throws an UnsupportedOperationException on empty collections. When the collection might be empty, prefer fold with an explicit initial value (like 0 or an empty string). Use reduceRight to process elements from right to left.

Intermediate

partition splits a collection into a Pair of two lists — the first list contains elements that satisfy the predicate and the second contains elements that do not. Example: val (adults, minors) = users.partition { it.age >= 18 }. This is more efficient than calling filter twice (once for the matching elements and once for the non-matching) because it iterates the collection only once. Destructuring the resulting pair directly into two variables makes the intent very readable.

Intermediate

associateBy transforms a collection into a Map where the key for each element is derived by a key selector function. Example: val usersById = users.associateBy { it.id } returns a Map<Int, User>. If multiple elements share the same key, only the last one is kept. Use associate { element -> key to value } for full control over both key and value. For a Map where each key maps to a List of values (like groupBy), use groupBy instead of associateBy.

Intermediate

takeIf returns the object itself if the predicate is true, otherwise returns null. It allows conditional processing inline without breaking a chain of calls. Example: val validAge = age.takeIf { it >= 18 } ?: throw IllegalArgumentException("Must be 18+"). The complementary function takeUnless returns the object when the predicate is false. Both are useful for guarding against invalid values in a functional style without needing an if-else block, keeping the code concise and readable in transformation chains.

Intermediate

zip combines two collections into a list of Pairs, pairing elements at the same index. If the collections have different sizes, the result stops at the shorter one. Example: val pairs = listOf(1, 2, 3).zip(listOf("a", "b", "c")) gives [(1, "a"), (2, "b"), (3, "c")]. You can also pass a transform function: zip(other) { a, b -> a.toString() + b }. The inverse operation is unzip(), which splits a list of pairs into two separate lists.

Intermediate

chunked(size) splits a collection into a list of sublists, each with at most the specified number of elements. The last chunk may be smaller if the collection size is not evenly divisible. Example: listOf(1, 2, 3, 4, 5).chunked(2) gives [[1, 2], [3, 4], [5]]. This is useful for batch processing API requests or splitting a large dataset into manageable pages. For a sliding window over the collection, use windowed(size, step) instead.

Intermediate

Both List and Array hold ordered collections of elements, but they differ in several ways. An Array has a fixed size set at creation, is always mutable (you can update elements but not add/remove), and maps directly to Java arrays in the JVM bytecode. A List can be immutable (listOf) or mutable (mutableListOf), integrates with the full Kotlin collections API, and supports a richer set of operations. Prefer List for idiomatic Kotlin. Use Array or specialized arrays (IntArray, DoubleArray) when you need Java interoperability or the performance characteristics of primitive arrays.

Intermediate

coroutineScope { } is a suspend function that creates a new scope tied to the current coroutine. It suspends until all its child coroutines complete and participates in structured concurrency — if any child fails, all others are cancelled and the exception propagates to the caller. GlobalScope, on the other hand, is a top-level scope that lives for the entire application lifetime and ignores structured concurrency — coroutines launched in it become orphaned if the component that started them is destroyed, causing leaks. Never use GlobalScope in Android; always use lifecycle-aware scopes like viewModelScope or lifecycleScope.

Intermediate

The lazy delegate supports three thread safety modes. SYNCHRONIZED (default) uses a lock to ensure only one thread initializes the value — safe for multi-threaded access but with synchronization overhead. PUBLICATION allows multiple threads to initialize the value concurrently, but only the first result is used — suitable when initialization is safe to run multiple times. NONE has no synchronization overhead and should only be used when you are certain the lazy property will only be accessed from a single thread. Example: val data by lazy(LazyThreadSafetyMode.NONE) { expensiveComputation() }.

Intermediate

Covariance, declared with the out keyword, means a generic class is a producer of values of that type — it can only return (produce) values of that type, never accept (consume) them. This allows assignment from a subtype to a supertype reference: a List<Dog> can be assigned to List<Animal> because List is declared out T. interface Producer<out T> { fun produce(): T }. Covariance preserves the subtype relationship: if Dog is a subtype of Animal, then Producer<Dog> is a subtype of Producer<Animal>. This is why Kotlin's immutable collections are covariant.

Advanced

Contravariance, declared with the in keyword, means a generic class is a consumer of values of that type — it can only accept (consume) values but cannot produce them. This reverses the subtype relationship: a Comparator<Animal> can be used where a Comparator<Dog> is expected, because a comparator that can compare any Animal can certainly compare Dogs. interface Consumer<in T> { fun consume(value: T) }. If Dog is a subtype of Animal, then Consumer<Animal> is a subtype of Consumer<Dog> — the relationship is reversed (contra-variant).

Advanced

A star projection (*) is used when you want to refer to a generic type without specifying the type argument — similar to Java's ? wildcard. List<*> means "a list of some type I don't know." You can read elements as Any? but cannot write to the list. Each type parameter with different variance produces different star projection behavior: for an out T parameter, * acts like out Any?; for an in T parameter, * acts like in Nothing. Use star projections when the actual type argument is irrelevant to the operation you're performing.

Advanced

A value class (declared with @JvmInline value class) wraps exactly one property and is represented as the wrapped type at runtime — no wrapper object is created. This gives you type safety without memory overhead. Example: @JvmInline value class UserId(val id: Long). You cannot accidentally pass a ProductId where a UserId is expected, even though both wrap a Long. At the JVM level, the compiler inlines the underlying type wherever possible, so UserId is just a Long at runtime. Value classes are ideal for domain modeling where you want strong types without allocation cost.

Advanced

A Kotlin DSL (Domain-Specific Language) is an API designed using Kotlin language features to read like a natural language for a specific domain. The key features that enable DSLs are: lambdas with receivers (providing a specific this context inside a block), infix functions (readable binary operations), and operator overloading. Well-known Kotlin DSLs include Gradle build scripts (build.gradle.kts), Jetpack Compose, Ktor routing, and the HTML builder. They allow domain experts to write declarative configuration without boilerplate, while still being type-safe and IDE-supported.

Advanced

A function type with receiver is written as ReceiverType.(Parameters) -> ReturnType and describes a function where this inside the function body refers to an instance of the receiver type. This is the foundation of Kotlin DSLs and scope functions. Example: val appendHello: StringBuilder.() -> Unit = { append("Hello") } — inside the lambda, this is a StringBuilder. You call it on an instance: val sb = StringBuilder(); sb.appendHello(). The scope functions apply, with, and run all use function types with receivers internally.

Advanced

When a function is inline, lambdas passed to it support non-local returns — you can return from the outer function inside the lambda. However, if the lambda is passed to another execution context (like a Runnable, a callback, or stored for later execution), non-local returns are not safe. The crossinline modifier on a lambda parameter of an inline function prohibits non-local returns from that lambda, allowing it to be used in contexts that do not permit non-local returns, while still inlining the lambda's body for performance. It is a compromise between full inlining and the safety of noinline.

Advanced

When a function is marked inline, all lambda parameters are inlined at the call site by default. But an inlined lambda cannot be stored in a variable, passed to a non-inline function, or captured in a closure that outlives the call. The noinline modifier on a specific lambda parameter opts that particular lambda out of inlining, treating it as a regular function object. Use noinline when you need to store a lambda reference, pass it to a non-inline function, or return it from the function. The other (non-noinline) lambdas in the same function are still inlined.

Advanced

A Channel is a concurrent primitive for communication between coroutines — like a blocking queue but with suspend functions instead of blocking calls. One coroutine send()s values (suspending if the buffer is full), and another receive()s them (suspending if the channel is empty). Channels are hot and stateful. Types: Rendezvous (no buffer — sender suspends until receiver is ready), Buffered (fixed-size buffer), Unlimited, and Conflated (only keeps the latest value). For most use cases, SharedFlow is a better choice as it is broadcast-capable and does not need to be explicitly closed.

Advanced

A Mutex (mutual exclusion lock) is a coroutine-friendly synchronization primitive that protects shared mutable state from concurrent access. Unlike Java's synchronized block which blocks the thread while waiting for the lock, Mutex.withLock { }` suspends the coroutine, freeing the thread for other work. Example: val mutex = Mutex(); mutex.withLock { sharedList.add(item) }. This is crucial in coroutines because you might have many coroutines on a few threads, and blocking a thread waiting for a lock defeats the purpose of coroutines. An alternative is to confine mutable state to a single dispatcher (actor pattern).

Advanced

CoroutineExceptionHandler is a CoroutineContext element that catches uncaught exceptions from root coroutines (those started with launch in a scope). Install it when creating a scope or launching a coroutine: val handler = CoroutineExceptionHandler { _, e -> log(e) }. It is important to note that CoroutineExceptionHandler does NOT work with async — exceptions from async are stored in the Deferred and re-thrown when you call await(), where you should use a try-catch. Also, it does not work with child coroutines — exceptions from child coroutines propagate to the parent, not to the handler.

Advanced

Kotlin Multiplatform allows sharing Kotlin code across multiple platforms — Android, iOS, web, desktop, and server — from a single codebase. The shared code (business logic, networking, data models, repositories) lives in the commonMain source set and compiles to JVM bytecode for Android/JVM, LLVM bitcode for iOS/Native, and JavaScript for web. Platform-specific code uses the expect/actual mechanism. KMP is not a UI framework — it shares logic, not UI. Combine it with Jetpack Compose (Android) and Compose Multiplatform or SwiftUI (iOS) for UI. KMP is production-ready and used by major companies like Netflix and VMware.

Advanced

The expect/actual mechanism is Kotlin Multiplatform's way of declaring a platform-agnostic API in common code while providing platform-specific implementations. In commonMain, you write expect fun getPlatformName(): String — a declaration without an implementation. In androidMain, you write actual fun getPlatformName() = "Android ${Build.VERSION.SDK_INT}", and in iosMain, actual fun getPlatformName() = "iOS ${UIDevice.currentDevice.systemVersion}". The compiler enforces that every expect declaration has a corresponding actual on every target platform.

Advanced

Both can have abstract methods and default implementations, but they differ in key ways. An abstract class can have constructors, state (backing fields), and init blocks — it models a strong "is-a" relationship. A class can extend only one abstract class. An interface cannot have constructors or store state (interface properties have no backing field), but a class can implement multiple interfaces. Use abstract classes for shared state and initialization logic; use interfaces to define capabilities (Serializable, Clickable) or when multiple inheritance of type is needed. Interfaces compiled with default implementations produce Java 8 default methods on the JVM.

Advanced

The internal modifier makes a declaration visible within the same compilation module — a module being a set of Kotlin files compiled together (typically a Gradle module). Unlike private (file/class scope) or public (visible everywhere), internal is the ideal visibility for library implementation details. Library code marked internal can be used freely across the library's own modules but is invisible to library consumers who depend on the library from the outside. This enforces a clean public API boundary without making internal helpers private to a single file.

Advanced

The tailrec modifier allows the compiler to optimize a tail-recursive function into an iterative loop, preventing stack overflow for deeply recursive calls. A tail-recursive function is one where the recursive call is the very last operation — there is no additional computation after the call. Example: tailrec fun factorial(n: Long, acc: Long = 1): Long = if (n == 1L) acc else factorial(n - 1, n * acc). Without tailrec, deep recursion would throw a StackOverflowError. The compiler generates a warning if you mark a function with tailrec but it is not actually tail-recursive.

Advanced

SharingStarted is a strategy that controls when a shared Flow (via stateIn() or shareIn()) starts and stops collecting from its upstream Flow. SharingStarted.Eagerly starts collecting immediately and never stops. SharingStarted.Lazily starts when the first collector appears and never stops. SharingStarted.WhileSubscribed(5000) starts on the first collector and stops 5 seconds after the last collector disappears — this is the recommended choice for Android ViewModels because it cancels the upstream (and API calls) when the UI goes to the background, saving resources, while the 5-second grace period handles screen rotations.

Advanced

Flow.combine() merges the latest emissions from multiple Flows using a transform function, emitting a new combined value whenever any of the source Flows emits a new value. combine(flowA, flowB) { a, b -> "$a $b" }. Unlike zip, which waits for both flows to emit before combining, combine always uses the latest value from all other flows. This makes it ideal for combining multiple independent StateFlows in a ViewModel into a single UI state: combine user preferences, loaded data, and loading state into one UiState object that the UI observes.

Advanced

Flow.stateIn(scope, started, initialValue) converts a cold Flow into a hot StateFlow that caches the latest value and shares emissions among all collectors within the given scope. It is the standard pattern for exposing repository Flows as StateFlow in a ViewModel: val uiState = repository.dataFlow.stateIn(viewModelScope, SharingStarted.WhileSubscribed(5000), InitialState). The started parameter (a SharingStarted strategy) controls the upstream collection lifecycle, and initialValue is what new collectors receive before the first emission from the upstream Flow arrives.

Advanced

buildList (along with buildMap and buildSet) creates an immutable collection using a builder DSL block. Inside the block, you use a MutableList to add elements, and the result is a read-only List. Example: val items = buildList { add("first"); addAll(fetchItems()); if (hasExtra) add("extra") }. This is cleaner than the alternative of creating a mutableListOf(), populating it with logic, then calling .toList(). The intent (building an immutable list) is immediately clear from the call site, and you cannot accidentally forget the .toList() conversion.

Advanced

When Kotlin calls Java code, Java types appear as platform types — indicated in the IDE with a ! suffix (e.g., String!). Platform types are neither nullable nor non-nullable in Kotlin's type system — the compiler does not enforce nullability for them, placing the responsibility on the developer. You can treat them as either nullable or non-nullable. To get proper nullability information, annotate Java code with @NotNull and @Nullable annotations (from JetBrains, Android, or JSR-305 packages) — Kotlin recognizes these and applies the corresponding nullability constraints, restoring compile-time null safety for Java APIs.

Advanced

An object expression creates an anonymous class instance on the fly, without declaring a named class. It can implement interfaces or extend a class: val listener = object : View.OnClickListener { override fun onClick(v: View) { handleClick() } }. Unlike Java anonymous classes, Kotlin object expressions can implement multiple interfaces simultaneously. Object expressions capture the surrounding scope (they are closures), so they can access local variables. Each use of an object expression creates a new anonymous class. Note: object expressions are different from object declarations (which are singletons).

Advanced

Kotlin contracts are a mechanism for library functions to communicate additional guarantees to the compiler about their behavior, enabling smarter analysis and smart casts. For example, a contract can tell the compiler that a lambda will be called exactly once, allowing the compiler to recognize that variables initialized inside the lambda are definitely initialized afterward. Or a contract can tell the compiler that if a function returns normally, a certain condition is true. Contracts are declared with the contract { } DSL block inside the function body. They are experimental/stable features used in the standard library (e.g., run, with, let use them internally).

Advanced

Flow.catch is an intermediate operator that catches exceptions thrown by the upstream flow and allows you to handle them — either by emitting a fallback value or logging the error. flow.catch { e -> emit(emptyList()) }.collect { }. Critically, catch only catches exceptions from the upstream — it does not catch exceptions thrown in the collector itself. For that, wrap the collect call in a try-catch. Combining catch with onEach and operators like retry gives you a complete error handling strategy for production flow pipelines.

Advanced

A cold flow (regular Flow) does not produce values until a collector subscribes to it — each new collector gets its own independent execution of the flow producer. This is like a music track: each listener plays it from the beginning. A hot flow (StateFlow, SharedFlow, Channel) is active independently of collectors — it produces values regardless of whether anyone is listening. Late subscribers may miss past emissions (configurable via replay buffer). StateFlow and SharedFlow are the recommended hot abstractions for sharing state and events in ViewModels, respectively.

Advanced

Both give alternative names to types, but they serve different purposes. A type alias (typealias UserId = Long) is purely a compile-time synonym — UserId and Long are completely interchangeable, providing no type safety between them. An inline/value class (@JvmInline value class UserId(val id: Long)) creates a distinct type in the type system — you cannot accidentally pass a ProductId where a UserId is expected even if both wrap a Long. At runtime, the value class is represented as the wrapped type (no boxing), so there is zero performance overhead. Use value classes for strong domain types; use type aliases only for readability.

Advanced

Jetpack Compose is Android's modern declarative UI framework built entirely in Kotlin. Instead of XML layouts and imperative View manipulation, you describe UI as composable functions annotated with @Composable. Compose leverages Kotlin deeply: coroutines for animations and async UI, extension functions and DSLs for layout APIs, lambdas for event handlers, and higher-order functions for customizable components. The Compose compiler plugin transforms @Composable functions at compile time, injecting a hidden Composer parameter that tracks state and enables recomposition — rebuilding only the parts of the UI whose inputs have changed.

Advanced
Back to All Topics 100 questions total