Kotlin interview bank
Questions interviewers actually ask, each answered at the length you would say it aloud, with the topic to reread if you were not sure.
Language core
6 questions · 0 Seen01 What is the difference between val, var, and an immutable object in Kotlin? reveal ▾ hide ▴
val makes a reference read-only after initialization, while var allows the reference to be reassigned. Neither keyword decides whether the referenced object can change. A val can point to a MutableList whose elements are still added or removed, and two references may observe the same mutations. Immutability comes from the object model and the API that exposes it. I start with val to reduce assignment sites, then separately decide whether callers need a mutable object, a read-only view, or a defensive copy.
09 What does Kotlin generate for a data class, and which properties participate? reveal ▾ hide ▴
Kotlin derives equals, hashCode, toString, ordered component functions, and copy from properties declared in the primary constructor. Body properties remain ordinary instance state but are excluded from those generated members, so a copy initializes them again. I treat the constructor as the value-semantics boundary: moving a property across it changes comparison, hashing, logs, destructuring, and copying together. A data class can explicitly provide equals, hashCode, or toString, but custom equality must preserve the hash contract; copy and generated component functions cannot be supplied manually.
13 What contract must a Kotlin property delegate provide? reveal ▾ hide ▴
A read-only delegated property needs an operator getValue whose receiver is the delegate, whose arguments describe the owner and KProperty, and whose return fits the property type. A mutable property also needs setValue with a value parameter that can accept the property type. Implementing ReadOnlyProperty or ReadWriteProperty is optional but makes this contract explicit. I choose a specific owner type when the delegate depends on that owner, and Any? only for a truly general delegate. At a call site, by selects these operators at compile time; it does not use a naming convention discovered dynamically.
14 How do lazy, observable, and vetoable differ? reveal ▾ hide ▴
lazy is a read-only delegate that computes on first access and caches the first successful result. Its thread mode controls initialization and publication; failed initialization is retried on a later read. observable is read-write and invokes its callback after storing the new value, so it can notify but cannot roll back automatically. vetoable invokes a predicate before storage and keeps the old value when the predicate returns false. I use these only when their timing is the intended API. Validation that needs a reason or an atomic multi-field update belongs in an explicit method rather than a silent veto.
16 What does provideDelegate add beyond getValue and setValue? reveal ▾ hide ▴
provideDelegate intercepts binding between an owner property and the object on the right of by. It runs while the owner is initialized and returns the actual delegate that later receives getValue and setValue calls. This allows a provider to validate a property name, owner type, annotation, or registry entry early, and to create separate delegate state for every property. I keep the check limited to binding metadata because the owner may be only partly initialized at that point. Runtime value validation still belongs in setValue or, when callers need a result, in an explicit method.
33 What is the practical difference between T and T? in Kotlin? reveal ▾ hide ▴
T excludes null, while T? adds null to the values the type can represent. A T value can be passed where T? is expected, but a T? value must be checked, safely accessed, defaulted, or rejected before code can use it as T. This distinction makes absence visible in API signatures and lets the compiler reject unchecked member access. It does not validate domain content: a non-null String can still be blank or malformed. I keep uncertainty at input boundaries, validate it there, and expose non-null domain values once absence has been resolved.
Type system
2 questions · 0 Seen02 How do nullable types and smart casts work together in Kotlin? reveal ▾ hide ▴
A type such as String? admits null, so Kotlin blocks ordinary member access until the code handles that possibility. A null check, type check, or early return can narrow the value on the remaining control-flow path; that compiler-proven narrowing is a smart cast. It works only when the value is stable between check and use. Mutable or open properties may change, so the compiler may reject a smart cast. Reading such a property into a local val both fixes the observed value and often makes the proof possible.
04 How does Kotlin handle numeric inference, conversion, and division? reveal ▾ hide ▴
An unsuffixed integer literal normally becomes Int when it fits, while a decimal literal defaults to Double. Kotlin does not implicitly widen an Int argument to Long or Double; the caller uses conversions such as toLong() or toDouble(), which keeps precision and range decisions visible. Arithmetic may combine numeric operand types through their operators, but assignment still has an expected type. Division between integer operands discards the fractional part. I choose the result type before arithmetic, test overflow boundaries, and make monetary units and rounding policy explicit.
Control flow
4 questions · 0 Seen03 Why is an exhaustive when useful for enums and sealed hierarchies? reveal ▾ hide ▴
An exhaustive when makes every value in a closed state set explicit. When it is used as an expression, Kotlin requires all possible states to produce a result. Listing each enum member or sealed subtype without a broad else lets the compiler detect future additions: adding a state breaks every decision point that has not defined its behavior. An else is appropriate for an open input such as arbitrary text, but over a closed domain it often hides missing business logic and converts a useful compile-time failure into an unintended fallback.
30 When can a return inside a Kotlin lambda exit its enclosing function? reveal ▾ hide ▴
A bare return can exit the enclosing named function only when the lambda is passed through an inlinable path, such as a direct call to inline forEach. That is a non-local return because inlining puts the lambda body at the call site. An ordinary non-inline callback cannot do this, and crossinline explicitly prohibits it. To return only from the lambda invocation, I use return@label; an anonymous function makes an unqualified return local to itself. For complex early-exit logic, an ordinary loop is often clearer and avoids depending on inline control-flow rules.
44 How do inlining and contracts affect Kotlin scope-function blocks? reveal ▾ hide ▴
The five standard scope functions are inline, and their implementations call the block in place exactly once per scope-function invocation. Their contracts expose that call shape to compiler analysis, but they are not runtime guards for business side effects and say nothing about how often the surrounding function is called. Inlining also permits a bare return in a block to exit the enclosing named function, which is a non-local return. I use return@let or another label for a local exit, recompile after moving code to a different higher-order function, and avoid making performance claims without a benchmark.
47 Why can an else branch be harmful in a when over a sealed type? reveal ▾ hide ▴
A broad else satisfies exhaustiveness without naming which variants it handles. If the hierarchy later gains a state, old code still compiles and silently sends that state through the fallback, even when the business rule should differ. By listing the sealed cases and omitting else, I turn subtype addition into a compilation-driven migration list. I keep else only when unknown implementations or values are genuinely part of the boundary, such as an open protocol. The compiler proves type coverage, not branch correctness, so I still test representative payloads and transition rules for every leaf.
Collections
3 questions · 0 Seen05 Why is a Kotlin read-only collection not necessarily immutable? reveal ▾ hide ▴
List, Set, and Map expose read operations but do not guarantee that the backing object cannot change. A MutableList can be assigned to a List reference, and another alias can still add or remove elements; the List view then observes those changes. val only prevents the reference from being reassigned. At an API boundary I distinguish a live read-only view from a stable snapshot. If the caller needs a snapshot, I copy the collection, then decide separately whether mutable element objects also require copying.
08 Why should hash-based collection keys remain stable? reveal ▾ hide ▴
HashSet and HashMap choose a storage bucket from hashCode and confirm matches with equals. If a property used by those methods changes after insertion, a lookup using the object’s new state can search a different bucket. The entry may still occupy the old bucket but become difficult to find or remove. Kotlin data classes make this easy to miss because primary-constructor properties participate in generated equality and hashing. I use immutable identity fields or dedicated key values, and I test membership after any mutation that the model permits.
11 Why are mutable data classes dangerous as HashMap keys? reveal ▾ hide ▴
A HashMap chooses a bucket from hashCode and then uses equals to confirm a match. Generated data-class equality and hashing include primary-constructor properties. If one of those properties, or a nested value contributing to its hash, changes after insertion, lookup can search a different bucket while the entry remains in the old one. The key may become hard to find or remove. I use a dedicated key type with stable val properties and immutable reachable state. If identity is independent of mutable fields, I model that identity explicitly instead of hashing the whole entity snapshot.
Collection processing
1 question · 0 Seen06 When would you use Sequence instead of eager collection operations? reveal ▾ hide ▴
I choose Sequence when evaluation semantics matter: a pipeline can short-circuit, should avoid materializing intermediate results, or reads from a source that produces values on demand. Sequence intermediate operations such as map and filter are lazy; a terminal operation such as first or toList starts consumption. I do not assume it is faster, because lazy dispatch has overhead and stateful steps may need the whole upstream. I also verify that input is finite when required, resources remain open during consumption, and the source supports any repeated traversal the caller expects.
Maps and grouping
1 question · 0 Seen07 What happens when associateBy produces duplicate keys, and how do you handle it? reveal ▾ hide ▴
associateBy returns one value per key, so a later element with the same key replaces the earlier value. That is safe only when “last value wins” is the intended conflict policy. If keys must be unique, I reject a duplicate while inserting or compare the input and result sizes and report the conflicting key. If one key legitimately owns several values, I use groupBy and return Map<K, List
Object modeling
5 questions · 0 Seen10 Is a Kotlin data class copy a deep copy, and how do you isolate nested state? reveal ▾ hide ▴
copy is shallow. It constructs a new outer instance, defaults each primary-constructor argument to the current property, and passes nested references through unchanged unless the caller replaces them. Two copies can therefore share the same MutableList or mutable child object. For immutable nested data, I update by calling copy explicitly at each affected level. For mutable graphs, I first define ownership, then copy the collection structure and any mutable elements that require isolation. A read-only List type narrows available operations but does not prove that another alias cannot mutate its backing object.
15 How can a custom property delegate accidentally share state? reveal ▾ hide ▴
A delegate that stores one value in its own field shares that value with every property bound to the same delegate instance. Making it an object or placing one instance in a top-level variable can therefore couple unrelated fields and owners. I normally create a stateful delegate in each property declaration, which gives every owner-property pair its own instance. If sharing is intentional, the delegate needs an explicit key such as both thisRef and KProperty, plus a lifetime and concurrency policy. I test by interleaving writes across two properties on two owner instances; a single-owner test misses the leak.
37 How does encapsulation help a Kotlin class preserve its invariants? reveal ▾ hide ▴
Encapsulation makes the class the owner of its valid state transitions. I keep mutable representation private, validate constructor inputs, and expose operations such as reserve or withdraw instead of unrestricted setters. Each operation checks its preconditions and either leaves the object valid or fails without a partial update. A public val is not enough when it refers to a mutable collection, so I also consider aliases and defensive copies. Tests should attack the boundary: use invalid construction, rejected operations, and mutable inputs, then verify every observable state still satisfies the invariant.
38 When would you choose an interface over an abstract class in Kotlin? reveal ▾ hide ▴
I start with an interface when I need to name a role that unrelated classes can implement, especially when a class may need several such roles. Interfaces can declare abstract members and default behavior but do not own constructor state or backing fields. I choose an abstract class when implementations share per-instance state, construction rules, protected helpers, or a template whose order must stay fixed. Because a class has only one class superclass, that choice spends more of the type hierarchy. In both cases I document the replacement contract instead of opening members merely for tests.
40 What does "composition over inheritance" mean in a Kotlin design review? reveal ▾ hide ▴
It means I model a needed capability as a collaborator the object has, often behind a constructor-injected interface, unless the domain truly has an “is a” relationship. Composition keeps the replacement boundary narrow, allows independent lifetimes, and avoids inheriting unrelated state or open members. Inheritance still fits a stable substitutable hierarchy or a shared template protocol; it is not forbidden. I check whether every derived instance satisfies the base contract at every base-typed call site. If the goal is only to reuse logging, retry, or formatting code, a composed helper is usually the clearer choice.
Equality and hashing
1 question · 0 Seen12 How do array properties affect equality in a Kotlin data class? reveal ▾ hide ▴
Generated data-class equality delegates to each property type, and Kotlin arrays use reference-based equals rather than element-wise equality. Two Packet values holding separately allocated ByteArray instances can therefore compare unequal even when every byte matches. I prefer List when the property conceptually represents a value collection. If an array is required, I override equals and hashCode together, pairing contentEquals with contentHashCode. Nested arrays need contentDeepEquals and contentDeepHashCode. I also test two independently allocated equal-content arrays, because reusing one array reference would hide the bug.
Functions and receivers
7 questions · 0 Seen17 How does a lambda with receiver support a Kotlin internal DSL? reveal ▾ hide ▴
A receiver function type such as ServerBuilder.() -> Unit gives the lambda a ServerBuilder as this. Its members can then be called without an explicit parameter, and trailing-lambda syntax lets an entry function read like server { … }. Nothing is parsed as a new language: the entry function creates a builder, invokes the function value on it, validates state, and returns a result. I keep the receiver surface narrow because every public member becomes vocabulary inside the block. The final result should normally be a separate type so temporary mutation does not escape.
18 What problem does @DslMarker solve, and what does it not solve? reveal ▾ hide ▴
@DslMarker controls implicit receiver resolution in nested DSL scopes. When receiver types carry the same marker, the closest receiver remains available implicitly and calls through an outer receiver require a qualified this. That prevents a nested block from silently invoking a structurally inappropriate outer operation. The annotation does not remove the outer object, enforce runtime value rules, sanitize strings, or make results immutable. I apply one marker consistently across every receiver type in the logical DSL and keep negative compilation tests that prove invalid nesting is rejected.
21 Why are Kotlin extension functions not polymorphic? reveal ▾ hide ▴
An extension call is resolved from the receiver expression type known at compile time, together with visible declarations and arguments. The runtime subtype does not select a more specific extension. If an Alert value enters a parameter declared as Message, message.kind() resolves to the Message extension even when an Alert extension exists. I use an open member or interface operation when behavior must vary by runtime subtype. I test extension adapters through concrete, base, and interface-typed variables because concrete-only tests hide this distinction.
22 What happens when an extension and a real member have the same signature? reveal ▾ hide ▴
The applicable real member wins; an extension cannot override or replace it. Different signatures may still coexist as overloads. This rule also matters during API evolution: a dependency may add a member whose signature matches an extension already used by callers, and recompiled source then selects the new member. I avoid broad names on widely used receiver types, watch shadowing warnings, and run call-site behavior tests when upgrading dependencies. If both operations are legitimate, I give the extension a domain-specific name rather than relying on subtle overload resolution.
29 How do Kotlin function types and expected types shape a lambda? reveal ▾ hide ▴
A function type states parameter types, the result, an optional receiver, and whether calls may suspend. The assignment target or receiving parameter supplies an expected type, which lets Kotlin infer lambda parameter types and often use the single name it. The final expression supplies the result unless control returns explicitly with a label. I add explicit types when a lambda has no useful context or overloads make several shapes possible. I also distinguish ((String) -> Int)?, a nullable function value, from (String) -> Int?, a function returning a nullable result.
41 How do you choose among let, run, with, apply, and also? reveal ▾ hide ▴
I choose on two axes. First, let, run, and with return the lambda result, while apply and also return the context object. Second, run, with, and apply expose that object as the receiver this; let and also pass it as a lambda argument, usually it. with is the one regular call that takes the object in parentheses, while the other four are extensions. I decide the required output type before considering style, then choose a receiver for member-heavy configuration or a named argument when several objects are in play. If neither stays clear, I use a local variable.
43 What makes nested Kotlin scope functions risky, and how do you review them? reveal ▾ hide ▴
Receiver-style blocks add implicit this values, and argument-style blocks often add an implicit it. With nesting, the nearest applicable receiver or lambda parameter can hide an outer one. Same-named properties make this worse because an assignment may compile while targeting the wrong object. I first name every it and annotate its static type. I label receiver lambdas and write this@label where ownership matters. Then I trace each read, write, and returned value. If the explanation needs frequent jumps across receiver levels, I flatten the code into local variables or extract a named helper.
API design
5 questions · 0 Seen19 Where should validation and ownership changes happen in a Kotlin builder DSL? reveal ▾ hide ▴
I treat build() as the boundary between incomplete, temporarily mutable assembly state and a valid result. The method checks constraints involving several fields, parses raw values into domain types, and copies builder-owned collections before returning. A read-only List type alone is not enough if it still aliases the builder’s MutableList. Per-field checks can run earlier when they improve the error location, but build must still guarantee every result invariant. I test missing and boundary values, then mutate the builder and original inputs after construction to prove the returned object is isolated.
20 When is a staged builder better than one mutable builder with nullable fields? reveal ▾ hide ▴
A staged builder fits a small, stable sequence of required steps. Each operation returns a type exposing only the next legal operations, and only the final stage can produce the result. Missing or out-of-order steps then fail during compilation instead of reaching a late !! or requireNotNull. I do not use stages for every optional flag or concrete value range because the number of types grows quickly and runtime checks remain necessary. Constructor parameters are simpler when there is no repeated structure; stages earn their cost when order itself is part of the domain contract.
24 How do you keep a Kotlin extension API predictable? reveal ▾ hide ▴
I start with ownership: the extension belongs near the domain rule that justifies it, not in a generic utilities package. I use the narrowest useful visibility, prefer explicit imports for contested names, and avoid vague extensions on broad receivers such as Any, String, or List
32 When should an API use a function type, a type alias, or a fun interface? reveal ▾ hide ▴
I use a function type when the API needs only a call shape, such as (Order) -> Boolean. A type alias can give that shape a domain name, but it creates no distinct type and remains assignment-compatible with the underlying function type. A fun interface creates a nominal SAM type, can hold non-abstract members and targeted extensions, and can improve a domain or Java interop boundary. It may also require conversions that a plain function type does not. I choose the interface for a real contract or role, not merely because SAM constructor syntax accepts a lambda.
36 When should an API return T? instead of a result type? reveal ▾ hide ▴
I return T? when absence is an expected outcome with one unambiguous meaning and the caller needs only value versus no value, such as a simple cache miss. I use a sealed result when callers must distinguish invalid input, not found, conflict, or dependency failure, because collapsing those cases into null loses the action they should take. A default with Elvis is appropriate only when omission and explicitly supplying that default are equivalent. Caller mistakes use requireNotNull, while broken object state uses checkNotNull; neither should replace a normal recoverable branch.
Properties and nullability
1 question · 0 Seen23 What constraints govern extension properties and nullable receivers? reveal ▾ hide ▴
An extension property adds accessor syntax but no storage, so it cannot have its own backing field. Its getter should normally compute a cheap, stable value from receiver state; hidden I/O or a global cache deserves an explicit function or state owner. A nullable receiver such as Customer? can be called directly when the value is null, with this remaining nullable inside. Adding ?. at the call site skips the body and may bypass its fallback. I test direct and safe calls separately and keep null behavior in the public contract.
Type modeling
3 questions · 0 Seen25 When should you choose an inline value class instead of a type alias or data class? reveal ▾ hide ▴
I use an inline value class when one underlying value completely describes a domain concept and I need a distinct static type, such as separating UserId from OrderId. A type alias only renames an existing type, so aliases with the same underlying type remain assignment-compatible and cannot prevent transposed arguments. A data class is a better fit when the value has several components, needs copy or destructuring, or benefits from an ordinary object representation. Possible unboxed representation is secondary: I choose the value class for the type boundary first, then verify boxing where performance actually matters.
27 Do inline value classes enforce invariants and immutability automatically? reveal ▾ hide ▴
No. An init block can reject invalid underlying values, but a companion factory does not become mandatory while the primary constructor remains public. I restrict constructor visibility when normalization or validation must be universal and route every public factory through the same checks. The val data property prevents reassignment of its reference; it does not freeze a MutableList or another mutable object behind that reference. Such mutation can change equality and hashing after insertion into a map. I prefer a stable immutable underlying representation, copy mutable inputs at the boundary, and test framework deserialization paths separately.
45 How do you choose among a sealed class, sealed interface, and enum in Kotlin? reveal ▾ hide ▴
I use an enum when the domain is a fixed set of singleton constants with one shared property shape. I use a sealed class when variants need different payloads and also share constructor state or implementation. I use a sealed interface when variants need different payloads but no shared construction, especially when one class must belong to several controlled classifications. All three can support exhaustive when. The deciding questions are payload shape, state ownership, and composition, not syntax preference. If external modules must add implementations, I choose a regular interface instead of either sealed form.
JVM representation
1 question · 0 Seen26 When does a Kotlin inline value class use a boxed representation? reveal ▾ hide ▴
On the JVM, the compiler prefers the underlying representation when a value is used directly as its value-class type. It normally needs the generated wrapper when the value is used as a generic type argument, an implemented interface, Any, or a nullable value-class type that the underlying value cannot represent. The same source value can therefore be unboxed at one call and boxed at the next. This is not a guarantee about heap allocation because later compiler and JIT optimization may remove objects. I inspect the target artifact and profile the actual call shape before changing an API for performance.
JVM interoperability
2 questions · 0 Seen28 How do JVM name mangling and boxed exposure affect Java callers of Kotlin value classes? reveal ▾ hide ▴
A value-class parameter may compile to its underlying JVM type, which can clash with an overload using that type directly. Kotlin avoids the clash by adding a stable hash to the method name, but the resulting name is awkward or impossible to call from Java source. I publish an explicit @JvmName bridge when Java can pass the underlying value and reconstruct the domain type immediately. When Java must hold the wrapper, Kotlin 2.4.10 offers experimental @JvmExposeBoxed or -Xjvm-expose-boxed. I treat those generated constructors and overloads as ABI, inspect them with javap, and compile a Java consumer test.
35 What is a Kotlin platform type, and how do you contain its null risk? reveal ▾ hide ▴
A platform type comes from Java when Kotlin lacks enough nullability information for the declaration. IDEs may display String!, but that exclamation mark is not Kotlin source syntax. The caller can treat the value as nullable or non-null and may call members directly, yet the Java implementation can still return null. I inspect supported nullability annotations and immediately assign an unannotated or untrusted result to an explicit T? in an adapter. The adapter then defaults, rejects, or maps the absence into a domain result before returning non-null data to core code.
State and concurrency
1 question · 0 Seen31 What risks arise when a Kotlin lambda captures mutable state? reveal ▾ hide ▴
A captured var belongs to the surrounding binding, so several callbacks can observe and update the same state. Copying a function-value reference does not clone that closure. This can create accidental cross-consumer state, and calls from several threads add races because capture provides no synchronization or atomicity. A retained callback also keeps captured objects reachable, potentially extending a request or service lifetime. I create one factory result per independent owner, capture small immutable snapshots when appropriate, define synchronization for intentional sharing, and test callbacks in interleaved order rather than only one at a time.
Type analysis
1 question · 0 Seen34 Why can Kotlin smart-cast a local val after a null check but reject the same check on a mutable property? reveal ▾ hide ▴
A smart cast is a control-flow proof, so the compiler must know the checked value cannot change before its use. A local val normally has one stable value. A mutable property can be changed by other code, and a custom getter can return a different value on every read, so checking property != null does not prove that a later property read is non-null. I read the property once into a local val and check that snapshot. If the operation must remain atomic against concurrent changes, a snapshot is not enough; synchronization belongs inside the state owner.
Inheritance and dispatch
1 question · 0 Seen39 How do open, override, and final override control runtime dispatch in Kotlin? reveal ▾ hide ▴
A regular class and its members are final by default. The base declaration must mark the class and replaceable member open, while the derived declaration must say override. Calls through a base or interface reference then select the override from the object’s runtime type. An override remains open to another subclass unless it is written as final override. I use that final form when one layer completes the protocol. I also avoid open calls from constructors because dispatch can reach a derived implementation before derived properties initialize, exposing default values or throwing unexpectedly.
Nullability
1 question · 0 Seen42 Why is let not inherently null-safe, and what does value?.let guarantee? reveal ▾ hide ▴
let is a generic extension that can be called on a nullable receiver, so value.let always invokes its block and the parameter may have type T?. In value?.let, the safe-call operator supplies the conditional behavior: null skips the call, while a non-null value enters the block as T. The whole expression can still be null because the receiver was null or because the block returned null. If those causes have different business meanings, I use explicit branches or a result type rather than attaching one Elvis fallback and losing the distinction.
Inheritance
1 question · 0 Seen46 What exactly does Kotlin restrict in a sealed hierarchy? reveal ▾ hide ▴
A direct subtype must be a named declaration in the same package and module as the sealed parent; it cannot be local or anonymous. The restriction does not automatically close every later generation. A direct child left final cannot be extended, a sealed child continues the controlled boundary, and an explicitly open child can have indirect subclasses wherever ordinary visibility permits. In multiplatform code, source-set rules add another boundary. I review the modifier on every direct child because saying only that the parent is sealed overstates what the compiler knows about individual concrete descendants.
Generics
1 question · 0 Seen48 Why do generic sealed result types often use out T and Nothing? reveal ▾ hide ▴
In a result hierarchy, the success branch produces T, while loading or failure branches usually produce no success value. Declaring Outcome
No questions match this filter.