Kotlin vs Scala Interview Questions — Live Practice & Comparison
Reviewed by Mark Dickie · Last updated
Kotlin and Scala are both statically typed languages that run on the JVM, differing mainly in their design philosophy: Kotlin aims for pragmatic Java interoperability with a gentler learning curve, while Scala pursues expressive type systems and functional purity that push the language further from Java's idioms. For interview prep, the two languages pull you toward different question clusters. Kotlin interviews lean on null safety, extension functions, coroutines, and Java interop gotchas — topics a Java shop adopting Kotlin will grill you on. Scala interviews reach for pattern matching, implicits (or givens in Scala 3), higher-kinded types, and the collections hierarchy, which probe whether you can reason about type-level abstractions.
| Aspect | Kotlin | Scala |
|---|---|---|
| Typical interview focus | Null safety, coroutines, Java interop, DSL building | Type system, pattern matching, functional patterns, implicits/givens |
| Interview difficulty range | Often lands in the 2–4 band | Tends toward 3–5, especially at type-system depth |
| Where it shows up | Android roles, backend shops migrating from Java | Data engineering, distributed systems, FP-leaning teams |
| Strength in interviews | Fast ramp if you know Java; fewer footguns | Demonstrates deep FP and type-theory grounding |
- If the role involves Android or a team actively migrating Java code, Kotlin is the more relevant bet — brush up on coroutines and null-safety contracts.
- If the job centers on data pipelines, Spark, or a team that values functional programming rigor, Scala questions will carry more weight.
- If you already write Java comfortably, Kotlin questions will feel familiar quickly; if you want to show type-system depth, Scala is where that pays off.
- Check the attempt-data table below for average scores and miss rates on each question set before deciding where to spend your practice time.
Kotlin vs Scala, side by side
How Kotlin and Scala compare on Tarmac’s interview questions.
| Metric | Kotlin | Scala |
|---|---|---|
| Practice questions | 6 | 6 |
| Average score | — | — |
| Hardest question (% who miss it) | — | — |
| Average time per question | — | — |
Practice questions
In Kotlin, a value class (declared with value class on the JVM) must have exactly one read-only property in its primary constructor.#
Options
- True
- False
Show answer
True. A Kotlin value class must declare exactly one read-only (val) property in its primary constructor; the compiler inlines this single underlying value wherever possible and rejects zero- or multi-property definitions at compile time.
The Kotlin language spec requires every value class to wrap exactly one underlying value, declared as a val property in the primary constructor. A value class with zero properties or with more than one property will not compile.
In Scala, given two case class instances created with identical field values, what does == return?#
Options
- false — they are different instances, so != is true
- Does not compile — you must override equals manually
- true — case classes compare by field values, not reference
- true only if both are declared val
Show answer
true — Scala case classes generate a structural equals that compares field values, so two instances with identical fields are equal regardless of being separate objects.
Case classes automatically generate a value-based equals method. Since p1 and p2 have the same x and y values, p1 == p2 evaluates to true. Reference identity is not used; the comparison is structural.
Which functions does the Kotlin compiler automatically generate for a data class based on the properties declared in its primary constructor?#
Options
- clone(), equals(), hashCode(), and toString()
- equals(), hashCode(), toString(), copy(), and componentN()
- equals(), hashCode(), toString(), and deepCopy()
- compareTo(), clone(), serialize(), and componentN()
Show answer
Kotlin's data class modifier causes the compiler to auto-generate equals(), hashCode(), toString(), copy(), and componentN() functions for the properties declared in the primary constructor. The componentN() functions enable destructuring declarations, and copy() creates a shallow copy with optional property overrides.
For each property in the primary constructor, the Kotlin compiler generates equals()/hashCode() based on those properties, toString() returning the class name and property values, copy() for creating a shallow copy with optional overrides, and componentN() functions to support destructuring declarations.
In Scala, how can a companion object access private members of its companion class (and vice versa) when the JVM itself would forbid it, and which modifier prevents even companion access?#
Show answer
The Scala compiler permits mutual access to private members between a class and its companion object defined in the same source file by emitting synthetic accessor methods (typically with package- or public-level JVM visibility) that bridge the gap — the access control is enforced at the source level, not the bytecode level. However, private[this] (object-private access) restricts visibility to the current instance only, so even the companion object cannot reach those members; no synthetic accessor is generated for private[this] fields.
Scala's private is a source-level concept: the compiler validates it during type checking but, for companions, generates synthetic accessors so the JVM allows the call. private[this] is stricter — it forbids access from any other instance, including the companion, and no accessor is emitted.
Place the following events in the order they occur when a Kotlin object declaration is first accessed at runtime.#
Put these in order
- The object's class is loaded and its static initializer is triggered on first access
- The declared superclass constructor (if any) executes
- Property initializers and init blocks execute in textual (top-to-bottom) order
- The fully initialized singleton instance is returned to the caller
Show answer
When a Kotlin object is first accessed, its class loads and the static initializer fires, then the superclass constructor runs, then property initializers and init blocks execute in top-to-bottom textual order, and finally the fully constructed singleton instance is returned to the caller. Kotlin objects are lazily initialized on first use.
Kotlin object declarations are initialized lazily — the singleton is not created until first access (the object's class is loaded and its static initializer is triggered on first access). When initialization begins, the superclass constructor runs first (the declared superclass constructor (if any) executes), mirroring normal class construction. Then property initializers and init blocks run in textual order (property initializers and init blocks execute in textual (top-to-bottom) order) — so x is initialized, then the init block runs, then y. Only after all initialization completes is the fully constructed instance handed back to the caller (the fully initialized singleton instance is returned to the caller).
Consider the following Scala trait hierarchy:#
Put these in order
- A
- B
- C
- D
Show answer
The linearization order of D extends B with C is D → C → B → A. Scala builds the linearization by starting with the class, then prepending the linearizations of the mixins from right to left, removing duplicates. So super.greet() in D calls C, which calls B, which calls A, producing "D->C->B->A".
Scala computes the linearization of D extends B with C as: start with D, then prepend the linearization of the rightmost mixin C (giving C, A), then prepend B's linearization (giving B, with A already present so it is removed). The result is D, C, B, A (followed by AnyRef, Any). When greet() is called, D.greet invokes super which resolves to C, which resolves to B, which resolves to A. So the invocation order from most-derived to least-derived is D → C → B → A.
In Kotlin, which standard-library function creates a read-only (immutable) List?#
Options
- listOf
- mutableListOf
- arrayListOf
- setOf
Show answer
listOf creates a read-only List<T> in Kotlin. The returned List interface exposes only read operations (get, size, contains, iteration) and no mutation methods, so the compiler rejects calls like .add() or .remove() at the call site. To get an in-place mutable collection you use mutableListOf, which returns a MutableList<T>.
listOf(...) returns List<T>, Kotlin's read-only list interface — it exposes get, size, contains, and iteration but no add/remove/set, so mutation calls are rejected at compile time. mutableListOf returns the mutable MutableList<T> interface; arrayListOf returns a java.util.ArrayList (mutable); and setOf creates a Set, not a List.
What does the following Scala code print? Assume Scala 2.13+ (LazyList from the standard library).#
var count = 0
val lzy = LazyList.from(1).map { x => count += 1; x * 2 }
val a = lzy.head
val b = lzy.head
println(count)Show answer
1
LazyList.from(1) produces an infinite lazy sequence starting at 1. The .map call is lazy — it returns a new LazyList without evaluating anything yet. The first lzy.head forces evaluation of the first element: the map function runs once (count becomes 1) and the result (2) is memoized in the LazyList cell. The second lzy.head returns the already-computed, cached head without re-invoking the map function, so count stays at 1. This memoization behavior is the key distinction between LazyList and a non-caching lazy view that recomputes on each traversal.
What does the following Kotlin code print?#
Options
- [10, 20, 30, 40, 50]
- [30, 40, 50]
- [3, 4, 5]
- [20, 30, 40]
Show answer
The code outputs [30, 40, 50]. filter { it > 2 } keeps only elements greater than 2 ([3, 4, 5]), and map { it * 10 } multiplies each remaining element by 10. Because filter runs before map, the original elements 1 and 2 are never transformed.
filter { it > 2 } keeps only the elements greater than 2 from the original list [1, 2, 3, 4, 5], producing [3, 4, 5]. Then map { it * 10 } transforms each of those elements by multiplying by 10, producing [30, 40, 50]. The answer is [30, 40, 50].
In Scala's standard collections library, which package does the Set type refer to by default when you write val s = Set(1, 2, 3) without any import?#
Options
- immutable.Set
- mutable.Set
- mutable.HashSet
- collection.mutable.ArrayBuffer
Show answer
By default, Set(1, 2, 3) in Scala produces an immutable.Set. Scala's Predef aliases Set to scala.collection.immutable.Set, so no import is needed for the immutable version. Mutable alternatives like mutable.Set or mutable.HashSet require an explicit import scala.collection.mutable.
Scala's Predef object provides an alias for scala.collection.immutable.Set, so writing Set(1, 2, 3) with no imports creates an immutable Set. The other options all come from the scala.collection.mutable package and require an explicit import to be used as Set.
The following Kotlin code fails to compile. What is the bug?#
sealed class Operation
data class Add(val a: Int, val b: Int) : Operation()
data class Subtract(val a: Int, val b: Int) : Operation()
data class Multiply(val a: Int, val b: Int) : Operation()
fun execute(op: Operation): Int = when (op) {
is Add -> op.a + op.b
is Subtract -> op.a - op.b
}Options
- The
whenexpression does not cover theMultiplysubclass, so it is non-exhaustive and the code will not compile. sealed classcannot havedata classsubclasses.executeshould returnInt?because no branch matches when the operation isMultiply.- Subclasses of a
sealed classmust be defined inside the class body.
Show answer
The when expression does not cover the Multiply subclass, so it is non-exhaustive and the code will not compile.
The when expression is used as the return value of execute, making it an expression form that Kotlin requires to be exhaustive. Operation is a sealed class with three direct subclasses: Add, Subtract, and Multiply. The when only handles Add and Subtract, omitting Multiply. Because sealed classes enable exhaustiveness checking, the compiler rejects the non-exhaustive when. Adding an is Multiply -> op.a * op.b branch (or an else branch) would fix the compile error.
In Scala 2.13+, applying .view to a strict List[Int] yields an instance of type scala.collection._____[Int]. This intermediate object stores the transformation pipeline but defers evaluation of map, filter, and other operations until a strict operation such as .toList or .force is invoked, thereby avoiding the creation of intermediate collections.#
Show answer
In Scala 2.13+, applying .view to a strict List[Int] yields an instance of type scala.collection.**View**[Int]. This intermediate object stores the transformation pipeline but defers evaluation of map, filter, and other operations until a strict operation such as .toList or .force is invoked, thereby avoiding the creation of intermediate collections.
In the Scala 2.13 collections redesign, calling .view on any collection returns a scala.collection.View[A]. A View is a lazy proxy: operations like map and filter on it produce new View instances that merely compose the transformation functions, without traversing the underlying data. The pipeline is only evaluated when a strict operation (e.g., .toList, .toVector, .force) pulls elements through the chain. This is distinct from the older Scala 2.12 Stream, which memoized elements, and from Iterator, which is consumed once. The type is scala.collection.View, not Stream, Iterator, or LazyList (which is the 2.13 successor to Stream for memoized laziness).