Overview
Kotlin interviews tend to circle back to the same handful of language features because they are exactly the areas where Kotlin diverges most sharply from Java: null safety, immutability, concise class declarations, and coroutines. Interviewers use these questions to check whether you understand *why* a feature exists, not just its syntax. This guide walks through the questions that come up again and again, with answers phrased the way you should phrase them out loud.
Cricket analogy: Like a bowling coach probing whether a trainee understands *why* a yorker works, not just how to bowl one, Kotlin interviews probe null safety, immutability, concise classes, and coroutines to test understanding of purpose, not just syntax.
Frequently Asked Questions
What is the difference between val and var?
var declares a mutable reference — you can reassign it after initialization. val declares a read-only (immutable) reference — once assigned, it cannot be reassigned. Note that val only makes the reference immutable, not necessarily the object it points to: a val list can still be a MutableList whose contents change, even though the variable itself can never point to a different object. Kotlin encourages val by default to reduce accidental mutation and make code easier to reason about.
Cricket analogy: Like a team's captaincy (var) that can be reassigned to a different player mid-series, versus a fixed squad list (val) that can't be swapped for a different squad object, even though players within that squad (a MutableList) can still be rotated in and out.
How does Kotlin's null safety system work?
Kotlin's type system distinguishes nullable types (String?) from non-nullable types (String) at compile time. A non-nullable type can never hold null, so the compiler rejects any assignment or dereference that could NPE without you explicitly handling it. This pushes null-pointer bugs from runtime crashes to compile-time errors, which is why Kotlin is often described as 'null-safe by design' rather than merely 'null-safe by convention.'
Cricket analogy: Like a scorecard field explicitly marked 'may be blank' (nullable String?) for a rain-abandoned match versus a mandatory 'final score' field that can never be empty (non-nullable String), Kotlin's compiler rejects any code that reads the mandatory field without checking, catching the bug before the match report ships.
What do ?., ?:, and !! do?
The safe-call operator ?. evaluates the expression to the right only if the receiver is non-null, otherwise it short-circuits to null. The Elvis operator ?: supplies a default value when the expression on its left is null, and is commonly chained after a safe call, e.g. user?.name ?: "Unknown". The not-null assertion operator !! forcibly casts a nullable value to non-null, throwing a NullPointerException immediately if it actually is null — it should be used sparingly, only when you can prove null is impossible.
Cricket analogy: Like checking nonStriker?.runs only if the non-striker is actually on the field (safe call), defaulting a rained-out score with ?: 0 (Elvis), or forcing captain!!.name when you're certain the captain is named, risking a crash if wrong.
val city: String? = user?.address?.city
val displayCity = city ?: "Unknown"
// !! throws NPE if user is null — avoid unless you are certain
val forced = user!!.nameHow do data classes differ from regular classes?
Marking a class 'data' tells the compiler to auto-generate equals(), hashCode(), toString(), a componentN() function per property (enabling destructuring), and a copy() function for creating modified shallow copies. A regular class gets none of this for free — you'd hand-write equals/hashCode/toString yourself. Data classes are meant for classes whose primary purpose is holding data; they require at least one parameter in the primary constructor and all primary-constructor parameters participate in the generated functions.
Cricket analogy: Like a standardized scorecard template that automatically generates the match summary, comparison, and a 'rematch with one lineup change' copy function, marking a Kotlin class data auto-generates equals(), hashCode(), toString(), componentN(), and copy(), while a regular class gets none of this for free.
When would you use a sealed class instead of an enum?
Enums represent a fixed set of instances where every case has the same shape (same properties). Sealed classes represent a fixed, closed set of subtypes where each subtype can carry different data and different structure — for example a Result sealed class with Success(data: T) and Error(message: String) subtypes that hold completely different fields. Because the subtype hierarchy is closed to the module/package that declares it, a when expression over a sealed class can be exhaustive without an else branch, which the compiler checks for you.
Cricket analogy: Like a fixed enum of MatchFormat (T20, ODI, Test, all sharing the same shape) versus a sealed MatchResult class where Win(margin: Int) and Abandoned(reason: String) carry completely different data; a when over the sealed class can skip else since the compiler knows every case.
What is an extension function and how is it dispatched?
An extension function lets you add a new function to an existing class without modifying its source or subclassing it, e.g. fun String.lastChar(): Char. Under the hood the compiler turns it into a static function that takes the receiver as its first parameter, so it never actually changes the class or has access to private members. Crucially, extension function resolution is static (resolved by the declared/compile-time type), not dynamic like real member functions — so if a subclass declares an extension with the same signature, the one that runs is chosen by the variable's static type, not its runtime type.
Cricket analogy: Like an unofficial commentator's stat (fun Batsman.strikeRate()) added on top of the official scorecard without modifying the board itself, Kotlin extension functions compile to static functions taking the receiver as a parameter, and which extension runs is decided by the declared type, not the actual player subtype at runtime.
What is the difference between lateinit and lazy?
lateinit lets you declare a non-null var without initializing it immediately, deferring initialization to later (commonly used for dependency injection or Android lifecycle fields); it only works on var properties of non-primitive, non-nullable types, and accessing it before initialization throws an exception. lazy is a delegated property built on a lambda that computes the value on first access and caches it thereafter; it works on val properties and is thread-safe by default (SYNCHRONIZED mode), making it the natural choice for expensive one-time computations.
Cricket analogy: Like a stadium announcing 'Player of the Match' as lateinit, the slot exists but stays empty until the match ends, and asking too early throws an error, versus the ground's lazy pitch report that's computed once on first request and cached for the rest of the day.
How do companion objects compare to Java's static members?
Kotlin has no 'static' keyword. Instead, a class can declare a companion object, which is a real singleton object tied to the class that can hold properties/functions callable without an instance (ClassName.member). Unlike Java statics, a companion object is an actual object — it can implement interfaces, have an instance passed around, and be extended with extension functions — giving you static-like access with more flexibility.
Cricket analogy: Like a franchise's official 'League Office' that isn't a player but is a real organizational entity you can address directly (LeagueOffice.announceRules()), Kotlin's companion object is an actual singleton tied to the class, unlike Java's static keyword, and it can implement interfaces too.
What is the difference between == and ===?
== calls the structural equals() function (checking value/content equality, with null-safety built in), which is roughly equivalent to Java's .equals(). === checks referential equality — whether both operands point to the exact same object in memory, equivalent to Java's ==. For data classes, == uses the generated equals() and compares property values, while === would only be true if both variables reference the identical instance.
Cricket analogy: Like comparing two players by their stats (==, structural, same runs and average counts as equal) versus checking if they're literally the same person walking onto the field (===, referential); two data-class Player objects with identical stats are == even if they're different instances.
data class Point(val x: Int, val y: Int)
val a = Point(1, 2)
val b = Point(1, 2)
println(a == b) // true — same property values
println(a === b) // false — different objects in memoryHow do coroutines differ from using threads directly?
A coroutine is a lightweight, suspendable unit of work managed by the Kotlin runtime, not the OS — thousands can run concurrently on a small pool of real threads because a suspended coroutine doesn't block its underlying thread, it simply yields it back. Threads are OS-level constructs that are comparatively expensive to create and always occupy their own stack while blocked. Coroutines use structured concurrency (scopes, jobs) to make cancellation and error propagation predictable, whereas raw thread management requires manual bookkeeping.
Cricket analogy: Like thousands of net-practice sessions sharing a handful of actual bowling machines by yielding turns rather than each session owning a dedicated machine, coroutines are lightweight and managed by the runtime, unlike OS threads which are expensive and each hold their own resources even while idle; structured concurrency (scopes, jobs) makes canceling a rained-out session predictable, unlike manually tracking every net session yourself.
What is the difference between primary and secondary constructors?
The primary constructor is declared in the class header (class User(val name: String, val age: Int)) and is the main entry point for initialization, often paired with init blocks. Secondary constructors are declared inside the class body with the constructor keyword and must delegate to the primary constructor (directly or transitively) via this(...) if a primary constructor exists. Secondary constructors are typically used to offer alternate ways to construct an object, such as for Java interop or overloaded construction patterns.
Cricket analogy: Like a player's core registration form (primary constructor: class Player(val name: String, val age: Int)) filled out once at signing, with alternate registration paths (secondary constructors) for transferred players that must still route through the same core form via this(...).
What is a smart cast?
After the compiler proves a nullable or supertype variable satisfies a check — such as an is check or a null comparison — it automatically treats the variable as the narrower, non-null type for the rest of that scope, without requiring an explicit cast. For example, inside if (obj is String) { obj.length } the compiler knows obj is a String and lets you call String members directly. Smart casts only work on val variables (or vars the compiler can prove aren't modified between the check and the use), because a mutable var could theoretically change type in between.
Cricket analogy: Like an umpire who, once confirming a delivery is a no-ball, automatically treats every subsequent call in that phase as a no-ball without re-checking, Kotlin's smart cast narrows obj to String inside if (obj is String) { obj.length }, but only for a val the compiler can prove won't change.
Quick Reference
- val = read-only reference, var = mutable reference; neither implies deep immutability of the object.
- Nullable types end in ? (String?); the compiler enforces null checks before dereferencing.
- ?. short-circuits to null; ?: supplies a fallback; !! throws NPE — reserve !! for provably safe cases.
- data class auto-generates equals/hashCode/toString/copy/componentN; requires at least one constructor parameter.
- sealed class subtypes can differ in shape; enum entries share the same shape.
- Extension functions are resolved statically at compile time, based on the declared type.
- lateinit is for vars initialized later; lazy is for vals computed once on first access.
- Companion objects replace Java statics but are real singleton objects that can implement interfaces.
- == is structural equality (equals()); === is referential equality (same instance).
- Coroutines are lightweight and cooperatively scheduled; suspending doesn't block the underlying thread.
- Secondary constructors must delegate to the primary constructor via this(...).
- Smart casts require the compiler to prove the type/nullability can't change between check and use.
Key Takeaways
- Kotlin's null safety is enforced by the type system itself, catching NPEs at compile time instead of runtime.
- Data classes and sealed classes eliminate huge amounts of Java boilerplate for value holders and closed hierarchies.
- Extension functions add API surface without inheritance, but are statically dispatched — know the limits.
- lateinit and lazy solve different deferred-initialization problems; don't conflate them.
- Coroutines are a scheduling model built on lightweight suspension, not a replacement abstraction over raw OS threads.
Practice what you learned
1. What happens when you call !! on a null value in Kotlin?
2. Which functions does the Kotlin compiler NOT automatically generate for a data class?
3. Why can a when expression over a sealed class omit the else branch and still be exhaustive?
4. How is extension function resolution performed in Kotlin?
5. Which statement about lazy properties is correct?
6. What distinguishes === from == in Kotlin?
Was this page helpful?
You May Also Like
Null Safety in Kotlin
Kotlin's type system distinguishes nullable from non-nullable types at compile time, eliminating most NullPointerExceptions before code ever runs.
Sealed Classes in Kotlin
Sealed classes restrict a class hierarchy to a known, closed set of subclasses, enabling exhaustive when checks.
Data Classes in Kotlin
See how Kotlin data classes auto-generate equals, hashCode, toString, copy, and componentN functions for value-holding objects.
Coroutines in Kotlin
Understand how Kotlin coroutines enable lightweight asynchronous programming using suspend functions and builders.
Related Reading
Related Study Notes in Programming
Browse all study notesApache Spark Study Notes
Programming · 30 topics
ProgrammingApache Flink Study Notes
Programming · 30 topics
ProgrammingHadoop Study Notes
Programming · 30 topics
ProgrammingSnowflake Study Notes
Programming · 30 topics
ProgrammingApache Airflow Study Notes
Programming · 30 topics
Programmingdbt (Data Build Tool) Study Notes
Programming · 30 topics