Free Kotlin Interview Questions & Answers

Kotlin language essentials and the tricky corners interviewers actually probe.

All 50 questions and detailed answers are free. No account or sign-in required.

  1. Does declaring a variable with val make the object it references immutable?

    No, val only makes the reference itself read-only, meaning you cannot reassign the variable to point at a different object. The object it points to can still be mutable, so if you write val list = mutableListOf(1, 2, 3) you can still call list.add(4) because add mutates the underlying object without reassigning the reference. Immutability of the reference and immutability of the object are two separate concerns, and val only guarantees the former. To get true immutability you also need an immutable type, and even then only if the type has no hidden mutable state.

  2. Is a List returned by listOf actually immutable, and can it ever change under your feet?

    Kotlin's List is read-only, not immutable: the interface simply lacks mutating methods, but the object behind it can still be a MutableList that other code holds and modifies. A common trap is returning a MutableList upcast to List; a caller who kept the original mutable reference, or who casts your List back to MutableList, can change the contents. On the JVM listOf backs onto Java collections, so read-only is a compile-time contract enforced by the interface, not a runtime guarantee. For genuine defensive immutability you must copy the data or use a truly immutable collection library like kotlinx.collections.immutable.

  3. What is the difference between == and === in Kotlin?

    The == operator checks structural equality and compiles to a null-safe call to equals, so a == b becomes a?.equals(b) with a fallback of b being referentially null. The === operator checks referential identity, meaning whether two references point to the exact same object instance. For data classes == compares field values while === still compares identity, so two data class instances with equal fields are == but not ===. A frequent gotcha is boxed numbers: comparing two Int values that get autoboxed to Integer with === can be false even when == is true, because identity caching only covers a small range.

  4. What is a platform type, and why can it cause a surprise NullPointerException even in null-safe Kotlin?

    When Kotlin calls Java code whose nullability is unknown, the returned type is a platform type, written internally as something like String with a trailing exclamation mark that Kotlin does not force you to null-check. This lets you assign a platform type directly to a non-null Kotlin type without a warning, and if the Java value was actually null the NPE fires at the point of assignment or first use rather than at the Java boundary. The fix is to treat Java results skeptically: assign them to explicitly nullable types, or rely on JSR-305 and Jetpack nullability annotations that Kotlin honors to turn platform types into proper nullable or non-null types.

  5. When you call copy() on a data class holding a mutable list, is the copy independent of the original?

    No, copy performs a shallow copy: it creates a new data class instance but copies references to the same nested objects. If a property is a MutableList, both the original and the copy point to the same list instance, so mutating it through one is visible through the other. This surprises people who expect copy to give a fully independent snapshot. To get independence you must deep-copy the mutable members yourself, for example by passing a fresh list to copy, or better, design the data class with only immutable properties so the shallow copy is effectively deep.

  6. What is the practical difference between lateinit var and by lazy for deferred initialization?

    lateinit is a var you promise to assign before first read; it works only on non-null, non-primitive types, is mutable, can be reassigned or reset, and throws UninitializedPropertyAccessException if read too early. by lazy is a val computed once on first access and then cached, is thread-safe by default via LazyThreadSafetyMode SYNCHRONIZED, and can hold primitives and nullable types. Use lateinit when an external framework injects the value after construction, such as in dependency injection or view binding; use lazy when the value is self-contained and expensive to compute. You cannot use lazy on a var, and you cannot check lateinit's state except through the isInitialized backing reference.

  7. Do the scope functions let, run, also, apply, and with differ only in style, or is there a real semantic distinction?

    They differ in two concrete axes: how the context object is referenced and what the block returns. let and also expose the object as the argument it, while run, apply, and with expose it as the receiver this. As for return value, let, run, and with return the lambda result, whereas also and apply return the context object itself. So apply is for configuring an object and returning it, also is for side effects like logging while returning it, let is for transforming a value or scoping a nullable with a safe call, and run combines this-receiver with returning a computed result. Choosing the wrong one silently returns the wrong type.

  8. Why can an extension function not override a member function, and what determines which one is called?

    Extension functions are resolved statically at compile time based on the declared type of the receiver expression, not dynamically on the runtime type. They are compiled to static methods taking the receiver as a parameter, so they are not part of the class's virtual method table and cannot participate in polymorphism. If a class has a member function and an extension with the same signature, the member always wins. And if you have an extension on a base type and one on a subtype, the one chosen depends on the static type of the variable, so assigning a subtype instance to a base-typed variable invokes the base extension.

  9. What does the inline keyword actually do to a higher-order function, and what problem does it solve?

    inline instructs the compiler to copy the function's bytecode, and the bytecode of its lambda arguments, directly into each call site rather than allocating a function object per lambda. This removes the object allocation and virtual invoke overhead of passing lambdas, which matters in hot loops, and it is what enables non-local returns from lambdas and reified type parameters. The tradeoffs are code size growth if the function is large or called in many places, and that inlining is a compile-time substitution not a mere hint. You should reserve inline for small functions that take lambdas; inlining a big function with no lambda parameters gives little benefit and bloats the output.

  10. What do noinline and crossinline do, and when are they each required?

    In an inline function all lambda parameters are inlined by default. noinline marks a specific lambda parameter to not be inlined, which is required when you need to store that lambda in a variable, pass it to another non-inline function, or return it, since an inlined lambda has no object representation. crossinline keeps a lambda inlined but forbids non-local returns from it, which is required when the lambda will be invoked from a different execution context, such as inside a nested lambda or a Runnable, where a non-local return would be illegal. So noinline is about identity and passing lambdas around, while crossinline is about controlling return semantics while preserving inlining.

  11. What does a reified type parameter enable that a normal generic type parameter cannot, and what is the requirement?

    Normally generic type arguments are erased at runtime, so you cannot write T colon colon class or check value is T inside a generic function. Marking the parameter reified in an inline function makes the actual type token available at each call site because the compiler substitutes the concrete type when inlining, so you can do is T checks, class references for reflection, and instantiate-by-type helpers. The requirement is that the function must be inline, since reification depends on call-site code substitution; you cannot have a reified parameter on a normal function or store it for later. This is why utilities like filterIsInstance and Gson-style fromJson helpers are written as inline reified extensions.

  12. Why should you not add or remove elements from a MutableList while iterating it, and how does Kotlin behave?

    The default MutableList on the JVM is backed by ArrayList whose iterator is fail-fast: structurally modifying the list during iteration, other than through the iterator's own remove, triggers a ConcurrentModificationException because the iterator detects a mismatch in its internal modification count. A for loop over the collection uses that iterator, so calling list remove inside it is the classic trap. The correct approaches are to iterate with an explicit MutableIterator and call its remove, to use removeAll or removeIf with a predicate, or to build a new filtered list. This is a JVM collection contract, not something Kotlin's read-only List interface protects you from once you have a mutable reference.

  13. What is the difference between const val and a regular val, and where can const be used?

    const val is a compile-time constant whose value is inlined into every use site as a literal in the bytecode, so it must be a primitive or String known at compile time, and it can only be declared at the top level or inside an object or companion object, never on a local or a class instance property. A regular val is a runtime read-only property backed by a getter and initialized when its owner is constructed, so it can hold any type and any computed value. A subtle consequence of inlining is binary compatibility: if a library changes a const value, dependents compiled against the old value keep the old literal until recompiled. Also const cannot have a custom getter since there is no getter at all.

  14. When are you forced to override both equals and hashCode together, and what breaks if you do not?

    The contract is that equal objects must have equal hash codes, so whenever you override equals to define value equality you must override hashCode consistently, or objects that are equal will land in different hash buckets. If you break this, HashMap and HashSet misbehave: you can insert a key and fail to retrieve it with an equal-but-different instance, or get duplicate set entries. Data classes generate both from the properties in the primary constructor for you, but note that properties declared in the class body outside the constructor are excluded from both, which is a real gotcha. If you hand-write equals on a regular class, always pair it with hashCode built from the same fields.

  15. Why might comparing two Double values with == give a surprising result, especially with NaN?

    Floating point cannot represent many decimals exactly, so 0.1 plus 0.2 is not exactly 0.3 and an == comparison of computed doubles often fails; you should compare within an epsilon tolerance instead. NaN is stranger: by IEEE 754, NaN is not equal to anything including itself, so at runtime a primitive nan == nan is false. But Kotlin has a twist: when doubles are used as generic or boxed values, or as keys in a collection, Kotlin uses total-ordering equality where NaN equals NaN and is treated as greater than positive infinity, so a set can contain NaN and find it. So the same NaN comparison can behave differently depending on whether it is a primitive == or a boxed equals call.

  16. What is the difference between a sealed class and an enum, and when do you choose each?

    An enum defines a fixed set of singleton instances that all share the same shape and can only carry a fixed set of properties per constant. A sealed class or interface defines a closed set of subtypes known at compile time, but each subtype can be a different class with its own properties, can have multiple instances, and can itself be a data class. So use an enum for a simple closed enumeration of constants like directions or states with uniform data, and a sealed hierarchy when each case needs distinct fields, such as a network Result with Success carrying data and Error carrying an exception. Both give exhaustive when handling, but sealed types model heterogeneous variants that enums cannot.

  17. What is the difference between a companion object and a plain object declaration?

    An object declaration is a lazily-initialized singleton in its own right, created on first access, and is the idiomatic way to write a stateless helper or a single shared instance. A companion object is an object tied to an enclosing class, of which there can be only one per class, and it lets you call its members through the class name so it fills the role Java's static members play. The key distinctions are that a companion is initialized when its enclosing class is loaded and its members can be exposed as real JVM statics only if you add JvmStatic, whereas a standalone object is accessed by its own name. Companions can also implement interfaces and be referenced as Companion, which plain statics cannot.

  18. Why is a Kotlin companion object member not automatically a Java static, and how do you fix interop?

    A companion object is a real object instance held in a static field named Companion inside the class, so from Java a companion function is called as MyClass dot Companion dot foo rather than MyClass dot foo. To expose a true static method callable as MyClass dot foo from Java you annotate the member with JvmStatic, which generates a static bridge. Similarly a companion val is a property with a getter on the Companion instance, so to expose it as a static field you use JvmField for a plain field or JvmStatic for static accessors. Forgetting this makes Kotlin APIs awkward from Java, which is why library authors sprinkle these interop annotations on companion members.

  19. What is the difference between a Sequence and a List when chaining map and filter, and why does it matter for performance?

    A List processes each operation eagerly and to completion, so map over a million items then filter creates a full intermediate list of a million mapped elements before filtering. A Sequence is lazy: it processes elements one at a time through the whole chain, so each element flows through map then filter before the next begins, and no intermediate collections are allocated. Sequences also enable short-circuiting, so operations like first or take stop pulling elements once satisfied. However sequences add per-element overhead and are worse for small collections or when you need the whole result anyway; they win on large data, expensive chains, or early termination. Terminal operations like toList or sum are what actually drive the evaluation.

  20. Why can the compiler refuse to smart-cast a nullable var even right after a null check?

    Smart casting requires the compiler to prove the value cannot change between the check and the use. A var property, especially one that is not local, could be reassigned by another thread or by code in between, so the compiler cannot guarantee it is still non-null and refuses the smart cast. The same applies to properties with custom getters, since a getter could return a different value each call, and to properties declared in another module or an open class where a subclass could override behavior. The workarounds are to copy the value into a local val first and check that, or to use a safe call with let, giving the compiler a stable snapshot it can prove is non-null.

  21. What does declaration-site variance with out and in mean, and how does it differ from Java wildcards?

    Declaring a type parameter out T makes the class covariant so a Producer of Cat is a subtype of a Producer of Animal, allowed because T only ever appears in output positions like return types. Declaring in T makes it contravariant so a Consumer of Animal is a subtype of a Consumer of Cat, allowed because T only appears in input positions. Kotlin lets you declare this variance once at the class declaration, whereas Java forces you to write it at every use with wildcards like extends and super. Kotlin still supports use-site variance too, written as out and in on a specific usage, which compiles down to Java wildcards, but declaration-site variance removes that repetitive noise for the common cases.

  22. What is a star projection, and how is List of star different from List of Any?

    A star projection, written List with a star inside the angle brackets, means a list of some unknown but specific type; you can read elements as the upper bound, here nullable Any, but you cannot add anything except null because the compiler does not know the real element type. A List of Any is a list explicitly of Any, to which you can add any value because the element type is known to be Any. The distinction matters for safety: the star projection preserves the fact that there is a concrete but unknown type parameter, preventing you from inserting a String into what is actually a list of Int. Star projection is essentially Kotlin's equivalent of Java's unbounded wildcard.

  23. What is type erasure in Kotlin generics, and what specifically can you not do because of it?

    On the JVM generic type arguments are erased at runtime, so a List of String and a List of Int are both just List at runtime with no retained element type. Consequently you cannot do a runtime is check against a parameterized type like value is List of String, only against the erased List with a star, and you cannot overload functions that differ only in generic arguments because their JVM signatures collide. You also cannot create an array of a generic type directly. The escape hatches are reified type parameters in inline functions, which recover the concrete type at the call site, and passing an explicit Class or KClass token when you need the type at runtime for reflection or deserialization.

  24. What is a value class (formerly inline class), and when does it actually avoid allocation?

    A value class wraps a single value and is marked with the value keyword plus JvmInline; at runtime the compiler tries to represent it as the underlying value with no wrapper object, giving you a distinct type such as UserId around a Long without allocation overhead. The catch is that the boxing is only elided when the value is used as its own static type; the moment it is used as a supertype, as a nullable, put in a generic collection, or accessed via reflection, the compiler must box it into a real object. So a value class is great for type-safe wrappers in hot paths but does not guarantee zero allocation everywhere. It also generates mangled JVM names to prevent signature clashes with functions taking the underlying type.

  25. Can an extension function be called on a null receiver, and how does that work?

    Yes, if the extension is declared on a nullable receiver type such as nullable String, then inside the function this can be null and you must handle it, but the call itself does not NPE even on a null value. This is exactly how the standard library toString on a nullable and isNullOrEmpty work: they are extensions on nullable receivers that check for null internally. Because extensions are static functions with the receiver passed as an argument, calling one on null simply passes null as that argument. The trap is assuming every extension is null-safe; only those explicitly declared on a nullable receiver are, and calling a non-null-receiver extension through a nullable reference still requires a safe call.

  26. What is the difference between an inner class and a nested class in Kotlin?

    By default a class declared inside another class in Kotlin is a nested class, which is like a Java static nested class: it holds no reference to an instance of the outer class and cannot access the outer instance's members. Adding the inner keyword makes it an inner class that does hold an implicit reference to the enclosing instance and can access its members, and from inside it you refer to the outer instance with a labeled this. This is the reverse of Java's default, where a plain inner class is non-static. The practical consequence is memory: an inner class silently retains its outer instance, which can leak, for example holding an Activity, so prefer nested unless you truly need the outer reference.

  27. How does a suspend function work under the hood, and why can you only call it from a coroutine?

    The compiler transforms a suspend function using continuation-passing style: it adds a hidden Continuation parameter and rewrites the body into a state machine where each suspension point is a state, so the function can return, releasing the thread, and later resume where it left off by re-entering with the saved state. Because the caller must supply a Continuation and be able to handle a suspend-or-resume result, you can only call a suspend function from another suspend function or a coroutine builder that provides that machinery. suspend is therefore a language feature, not a library call: it changes the function's actual JVM signature by adding the Continuation parameter, which is why a suspend function seen from Java takes an extra argument and returns Object.

  28. What makes a when expression exhaustive, and when are you not required to write an else branch?

    A when used as an expression must be exhaustive, covering every possible value, and the compiler enforces this. For a sealed class or interface, listing every subtype makes it exhaustive without an else, and for an enum, covering every constant does the same. The big advantage is future safety: if you later add a subtype or enum constant, the previously exhaustive when becomes a compile error, forcing you to handle the new case. For a Boolean, covering true and false is exhaustive. For open types like Int or String you generally cannot enumerate all values so an else is required. Note that a when used only as a statement historically did not require exhaustiveness, but newer Kotlin warns and moves toward requiring it.

  29. What is the Nothing type in Kotlin and where does it show up usefully?

    Nothing is the type with no instances, representing a computation that never returns normally, such as a function that always throws or an infinite loop. Because it has no values, Nothing is a subtype of every type, which is why an expression that throws can appear anywhere a value is expected, for example on the right of an elvis operator as in value or else throw error. It is also connected to null in that the type of the null literal is nullable Nothing, and it drives control-flow analysis so the compiler knows code after a Nothing-returning call is unreachable. A function declared to return Nothing tells both the compiler and readers that it never completes normally, improving smart-casts and exhaustiveness reasoning.

  30. What does a Kotlin contract do, and why is require or isNullOrEmpty able to help smart-casting?

    A contract is a declaration in a function body that tells the compiler about the function's effects in a way the compiler cannot otherwise infer, such as that a boolean return implies the argument is non-null, or that a lambda is invoked exactly once. This powers smart-casts across function boundaries: for example the standard library's isNullOrEmpty carries a contract saying that if it returns false the receiver is non-null, so the compiler smart-casts after the check. require and checkNotNull have contracts that let the compiler treat the value as non-null afterward, and the callsInPlace contract lets you initialize a val inside a lambda because the compiler knows the lambda runs exactly once. Contracts are still an experimental but widely used feature, and writing a wrong contract can mislead the compiler into unsound assumptions.

  31. How does destructuring work in Kotlin, and why is it positional rather than by name?

    Destructuring like val name-and-age pair from a person compiles to calls to component1, component2, and so on, in order, so it is purely positional and ignores property names. Data classes auto-generate componentN for their primary-constructor properties in declaration order, which is why reordering those properties silently changes what each destructured variable receives, a real bug source. Because it is convention-based, any class or even a custom type can support destructuring by declaring operator componentN functions. For maps, iterating entries destructures into key and value via componentN on Map Entry. A safer alternative when order is fragile is to use named references or, for maps, delegate to the map by name rather than positional destructuring.

  32. What is a tailrec function, and what silently prevents the optimization?

    Marking a function tailrec tells the compiler to compile a properly tail-recursive function into an iterative loop, avoiding stack growth and StackOverflowError for deep recursion. The strict requirement is that the recursive call must be the very last operation in that branch, with nothing done to its result. Common things that silently break it are wrapping the recursive call in a try-catch, since the exception handler means the call is not truly in tail position, or doing arithmetic on the result like returning n times a recursive factorial call. If the call is not in tail position the compiler cannot apply the optimization; in that situation Kotlin emits a warning rather than an error, so the function still compiles but recurses normally and can overflow. Always verify the warning is absent.

  33. How do property delegates like by lazy and Delegates.observable actually work?

    Property delegation desugars into the compiler generating a hidden field holding the delegate object and routing the property's get and set to the delegate's getValue and setValue operator functions, which receive the owner and a KProperty metadata object. by lazy returns a Lazy whose getValue computes and caches the value on first read. Delegates observable wraps a value and calls your callback after each assignment, while Delegates vetoable can reject an assignment by returning false from the callback. You can also delegate to a Map so that property reads pull from map entries by the property name, useful for parsing JSON into a typed view. Because the delegate is a real object with defined operator contracts, you can write your own to add validation, logging, or storage-backed properties.

  34. What is a typealias, and what does it not give you compared to a value class?

    A typealias introduces an alternative name for an existing type; it is a pure compile-time shorthand that is fully interchangeable with the original type and provides no new type safety at all. So a typealias UserId equal to Long means a UserId and a Long are the same type and can be passed for one another freely, which does nothing to prevent mixing a user id with an order id. A value class, by contrast, creates a genuinely distinct type the compiler will not let you swap for the underlying Long, giving real type safety at the cost of possible boxing. Use a typealias to shorten a verbose generic or function type for readability, and a value class when you actually want the compiler to enforce that two same-underlying types are not confused.

  35. How does operator overloading resolve in Kotlin, and what are the constraints on which functions you can overload?

    Operators map to specific named functions marked with the operator modifier, so plus maps to the addition operator, get and set map to indexing brackets, invoke maps to calling an instance like a function, and compareTo powers the ordering operators. You cannot invent new operators or change an operator's precedence and associativity; you can only give meaning to the fixed set by implementing the correspondingly named function with the right signature. The augmented assignment operators have a subtlety: plusAssign mutates in place while plus returns a new value, and if both are applicable for a mutable target the compiler reports ambiguity. Equality operators route through equals and cannot be overloaded separately, and the ordering operators all derive from a single compareTo.

  36. What is the difference between JvmField, JvmStatic, and JvmOverloads, and when do you need each?

    JvmField exposes a property as a plain public field with no getter or setter, so Java code accesses it directly as a field rather than through generated accessor methods, useful for constants and simple data holders. JvmStatic generates true static members for companion or object members so Java can call them without going through the Companion or INSTANCE reference. JvmOverloads generates overloaded method signatures for a function with default parameter values, because default arguments are a Kotlin-only feature the JVM does not understand, so without it Java callers cannot omit the defaulted arguments. Each solves a specific interop mismatch, and none of them changes how the API behaves from Kotlin, so they are purely about making the compiled API ergonomic for Java consumers.

  37. What does a labeled return like return at a label do, and why is it needed inside a lambda?

    A bare return inside a lambda passed to a non-inline function is not allowed, and inside an inline function a bare return returns from the enclosing function, not the lambda, which is called a non-local return. A labeled return like return at forEach returns only from the lambda to continue the loop, acting like continue, so you need it when you want to skip an iteration inside forEach rather than exit the whole enclosing function. Labels can be implicit, using the function name that takes the lambda, or explicit, written with a name followed by the at sign before the lambda. This distinction trips people up because forEach looks like a loop but a plain return in it, under inlining, exits the outer function entirely, which is often not what they intend.

  38. Why does adding a non-constructor property to a data class exclude it from equals, hashCode, and copy?

    Data classes generate equals, hashCode, toString, componentN, and copy solely from the properties declared in the primary constructor. A property declared in the class body, even a val, is not part of that generated machinery, so two instances differing only in a body property compare as equal, hash the same, and copy will not accept or vary it. This is a frequent source of bugs where people add a field to a data class expecting it to participate in equality and it silently does not. If a property must contribute to identity, it belongs in the primary constructor; if it is genuinely derived state, keeping it in the body is correct precisely because it should not affect equality.

  39. How do default arguments interact with copy on a data class, and what is the pitfall?

    copy generates an overload where every parameter defaults to the current instance's corresponding value, so you only specify the properties you want to change. The pitfall arises when people confuse the data class's constructor defaults with copy's defaults: copy does not reset unspecified fields to the constructor's declared default values, it keeps the existing instance's values, which is the intended behavior but surprises those expecting a fresh object. Another subtlety is that copy is shallow, so specifying a new value for one property while leaving a mutable collection property untouched keeps sharing that collection with the source. And because copy relies on named and default arguments, it is not accessible in the same convenient form from Java, which lacks those features.

  40. Why can you not smart-cast a property declared with a custom getter, and what is the safe alternative?

    A custom getter is executed on every access and can return a different value each time, so even if one call returns non-null the compiler cannot assume the next call in the same block returns the same non-null value, and it therefore refuses to smart-cast. The same reasoning blocks smart-casting open properties and properties from other modules, since their behavior could be overridden or changed. The safe pattern is to read the property once into a local val, then check and use that local, because a local val is guaranteed stable and the compiler can prove it. Using the safe-call with let on the property achieves the same by capturing a single evaluation into the lambda parameter, giving a value the compiler knows is fixed and non-null.

  41. What is the difference between associate, associateBy, and associateWith, and where do you lose data?

    associate takes each element and produces a full key-to-value pair, associateBy uses each element as the value and computes only a key from it, and associateWith uses each element as the key and computes a value from it. The silent data-loss trap is common to all three: because the result is a Map, if two elements produce the same key the later one overwrites the earlier, so you can quietly end up with fewer entries than input elements when keys collide. If preserving all elements per key is what you want, groupBy is the correct choice since it collects a list of values per key. Choosing associateBy on a non-unique key is a frequent bug that shrinks your data without any error.

  42. What is the difference between flatMap and map plus flatten, and when does a wrong transform surprise you?

    flatMap maps each element to an iterable or sequence and concatenates the results into one flat collection, which is equivalent to map followed by flatten but done in one pass. A common surprise is that flatMap expects the transform to return something iterable; if you accidentally have a transform returning a nullable single value you would use mapNotNull instead, and mixing these up leads to type errors or empty results. On sequences flatMap stays lazy, flattening lazily as elements are pulled. Another subtlety is that flatMapIndexed exists when you need the index, and that flattening deeply nested structures requires repeated flatMap since a single call only removes one level of nesting.

  43. How does Kotlin's elvis operator combined with return or throw enable early exit, and why does it type-check?

    The expression value or else return, and value or else throw, works because return and throw have type Nothing, which is a subtype of every type, so the elvis operator's right side is type-compatible with whatever the left side would produce. This lets you write a null-guard that either yields the non-null value or aborts the current function, and crucially the compiler smart-casts the value to non-null on the lines after, since the only way execution continues is if the left side was non-null. This pattern is idiomatic for flattening nested null checks into a linear happy path. The same Nothing typing is why the standard library's error and TODO functions can be used as expressions anywhere a value is expected.

  44. What is the difference between a reified is T check and a plain Class comparison for generics, and what does filterIsInstance rely on?

    A plain value is List of String check is impossible because of erasure, but value is T inside an inline reified function works because the compiler substitutes the concrete non-generic type at the call site and emits a real instanceof. However reification only recovers the type argument at the point of inlining for that specific parameter; it does not defeat erasure of nested generics, so is List of T still cannot check the inner element type at runtime. filterIsInstance is implemented as an inline reified extension that uses the reified token to test each element, which is why it can filter a list to a given concrete type. For deeper runtime type information you must pass a Class or a KType, or use kotlin-reflect, since reification alone cannot see through nested generic parameters.

  45. Why does modifying a captured var inside a Kotlin lambda work, unlike Java's effectively-final rule?

    Java only allows a lambda to capture effectively-final local variables, so you cannot reassign a captured local. Kotlin removes that restriction: a lambda can read and write a captured var. The mechanism is that the compiler boxes the mutable local into a small holder object, a Ref wrapper, and both the enclosing code and the lambda share that wrapper, so writes are visible on both sides. The tradeoff is a hidden allocation and, more importantly, thread-safety concerns, because if the lambda runs on another thread the shared mutable state has no synchronization and can race. So while it is convenient for accumulating into a counter in a local loop, capturing a var across concurrent coroutines or threads is a subtle correctness hazard.

  46. What is the difference between an anonymous object expression and a lambda when implementing an interface, and when must you use each?

    A lambda can only implement a functional interface, a SAM interface with exactly one abstract method, and Kotlin's SAM conversion lets you pass a lambda where such an interface is expected, though notably SAM conversion applies to Java interfaces automatically and to Kotlin interfaces only if they are declared fun interface. An anonymous object expression, written as object followed by the interface, can implement multiple methods, extend a class, hold state across calls, and implement multiple interfaces, so you must use it when the interface has more than one method or you need per-instance state. A further subtlety is that a lambda passed repeatedly may reuse a single instance while each object expression creates a new instance, which matters if you rely on identity such as for listener removal.

  47. How does the by keyword implement interface delegation, and what surprising override behavior can it produce?

    Class delegation with by makes a class implement an interface by forwarding all interface calls to a supplied delegate instance, generating forwarding methods automatically so you get composition without boilerplate. The surprising behavior is that these generated forwarders call the delegate's own implementations, and if the delegate internally calls one of its own methods that you have overridden in the outer class, the delegate does not see your override; it calls its own version, because the delegate has no knowledge of the wrapper. In other words delegation is not inheritance and does not give you virtual dispatch back into the outer class. Also the delegate is captured at construction, so reassigning the property used to initialize it later does not change what the forwarders point to.

  48. What does the in and out variance actually forbid you from doing, illustrated with a covariant class trying to consume its type?

    Declaring a class covariant with out T means T may only appear in output positions, so the compiler forbids a method that takes a T as a parameter, because covariance would let you pass, for instance, a Producer of Cat where a Producer of Animal is expected and then feed it a Dog, breaking safety. Symmetrically, in T forbids T in return positions. This is not a style rule but a soundness guarantee enforced at compile time. The escape valve is that out T can still appear in an in position if you locally override variance with use-site in on that parameter, but doing so restricts callers accordingly. This is exactly why the read-only List is declared out E, allowing covariance, while MutableList is invariant since it both produces and consumes E.

  49. Why can two functions with the same name but different generic parameters clash on the JVM, and how do JvmName and mangling help?

    Because of type erasure, two functions that differ only in generic type arguments, such as one taking List of String and one taking List of Int, erase to identical JVM signatures and cause a platform declaration clash. The JvmName annotation resolves this by giving one of them a different name in the bytecode while keeping the Kotlin name, so both remain callable from Kotlin. Separately, value classes and internal members get automatically mangled JVM names, adding a hash-like suffix, to avoid accidental clashes and to prevent Java code from calling members it should not; this is why a value-class-parameter function looks oddly named from Java. Understanding this explains puzzling clash errors that make no sense at the Kotlin source level.

  50. How do coroutine structured concurrency, the Nothing type, and suspend combine to make cancellation cooperative, and what breaks it?

    Cancellation in coroutines is cooperative: cancelling a Job sets it to a cancelling state, but the coroutine only actually stops when it hits a suspension point that checks for cancellation, at which point suspend functions in kotlinx.coroutines throw CancellationException, whose handling is special-cased so it propagates without being treated as a failure. Because a suspend function is transformed into a continuation-passing state machine, each suspension is where the machinery can observe the cancelled state and throw. What breaks cancellation is a tight computational loop with no suspension point, or catching the CancellationException in a broad try-catch and swallowing it, since that stops the cancellation from propagating and can leak the coroutine. The remedy is to periodically call yield or ensureActive, and to always rethrow CancellationException when catching broadly.

Practice all Kotlin questions interactively

Search, filter, and mark questions complete in the free Preparation Path. You can start immediately without an account.

Open free Preparation Path

More Android interview topics