Kotlin OOP & Functional
Classes, data classes, sealed classes, objects, extension functions, scope functions
- 01
What are data classes in Kotlin and what does the compiler generate?
EasyA
data classexists to hold data, and the compiler generates its equality, printing, copying and destructuring for you.data class User(val id: Long, val name: String, val email: String) val u1 = User(1, "Alice", "alice@x.com") val u2 = u1.copy(email = "a@x.com") // new instance, only email changed println(u1 == u2) // false — emails differ println(u1.hashCode() == User(1, "Alice", "alice@x.com").hashCode()) // true … - 02
What are sealed classes / sealed interfaces in Kotlin?
MediumA sealed class/interface restricts who can extend it: direct subclasses of both a sealed class and a sealed interface must be declared in the SAME module and the SAME package.
sealed interface ApiResult<out T> { data class Success<T>(val value: T) : ApiResult<T> data class Failure(val code: Int, val message: String) : ApiResult<Nothing> data object Loading : ApiResult<Nothing> } … - 03
What is the difference between class, object, and companion object in Kotlin?
Mediumobjectdeclares a singleton,companion objectis a singleton bound to a class, andclassis the ordinary instantiable declaration.// object — singleton object Logger { fun log(msg: String) = println("[LOG] \$msg") } Logger.log("hello") … - 04
What are extension functions and properties in Kotlin?
MediumExtensions add functions or properties to existing types without modifying their source.
fun String.titleCase(): String = split(" ").joinToString(" ") { it.replaceFirstChar(Char::uppercase) } println("hello world".titleCase()) // Hello World // Extension property — no backing field, computed … - 05
What are scope functions (let, run, with, apply, also)?
MediumScope functions run a lambda in the context of an object, and they differ on two axes: how the object is referenced (
itvsthis) and what the lambda returns (the object itself vs the lambda result).// let — null-safe transform val len: Int? = name?.let { it.length } // also — side effect in a chain val token = login() .also { logger.debug("login result: \$it") } … - 06
Kotlin classes are final by default — how do
open class,abstract classandinterfacediffer?MediumKotlin makes every class final by default, so
open,abstractandinterfaceare three different ways of opting into extension.// open — opt in to inheritance open class Repository(open val baseUrl: String) { open fun fetch(): String = "data" fun health() = "ok" // final by default — not overridable } … - 07
Which visibility modifiers does Kotlin have, and what does
internalactually cover?EasyKotlin declarations are
publicunless you say otherwise, and its extra level isinternal, meaning visible everywhere inside the same compilation module.// Top-level private — visible in this file only private const val RETRY_LIMIT = 3 internal class UserDto(val id: Long, val name: String) // module-only type class UserRepository internal constructor( … - 08
In what order do the primary constructor, property initializers,
initblocks and a secondary constructor run?EasyInitialization runs top to bottom through the class body, and a secondary constructor's own body is the last thing to execute.
class User(val name: String, age: Int) { // 1. parameters bound val greeting = "Hello, $name" // 2. initializer init { // 3. init block require(age >= 0) { "age must be >= 0" } } … - 09
You chain
mapandfilterover a list. When does insertingasSequence()actually make it faster?MediumA collection chain materializes a new list at every step, while a sequence pushes one element through the whole chain before touching the next.
val names = users .asSequence() .filter { it.active } .map { it.name } .take(10) .toList() // nothing ran before this line … - 10
What does
class LoggingRepo(private val inner: Repo) : Repo by innergenerate, and where does that delegation stop helping?MediumInterface delegation makes the compiler generate every method of the interface as a one-line forwarding call to the given object, so a decorator costs one line instead of twenty.
interface Repo { fun load(id: Long): User fun refresh() fun clear() } … - 11
When can you pass a lambda where an interface is expected in Kotlin?
MediumA lambda converts to an interface only when that interface comes from Java with a single abstract method, or is a Kotlin interface declared
fun interface.fun interface Validator { fun check(input: String): Boolean } val notBlank = Validator { it.isNotBlank() } // SAM constructor fun register(v: Validator) { } … - 12
How do you make
+,[]andinwork on a type of your own?MediumKotlin maps each operator to one fixed function name, and marking that function
operatoris the entire opt-in.@JvmInline value class Money(val cents: Long) : Comparable<Money> { operator fun plus(other: Money) = Money(cents + other.cents) operator fun times(factor: Int) = Money(cents * factor) operator fun unaryMinus() = Money(-cents) override fun compareTo(other: Money) = cents.compareTo(other.cents) … - 13
Your totals screen crashes only for brand-new users on
orders.map { it.total }.reduce { a, b -> a + b }— why, and what do you write instead?Easyreducetakes the first element as its seed, so an empty collection has nothing to start from and it throwsUnsupportedOperationException: Empty collection can't be reduced.data class Order(val id: Long, val customerId: Long, val total: Int) val orders: List<Order> = emptyList() // ❌ UnsupportedOperationException: Empty collection can't be reduced. val crash = orders.map { it.total }.reduce { acc, t -> acc + t } … - 14
The backend ships a new order status and the app starts crashing inside
OrderStatus.valueOf(dto.status)— how do you parse an enum the client does not know yet?EasyvalueOfis strict: any name that is not declared throwsIllegalArgumentException, so an enum is the wrong place to put a value the server is free to extend without shipping you a new build.enum class OrderStatus { PENDING, SHIPPED, DELIVERED, UNKNOWN } // ❌ takes down the whole parse on a value shipped after your release val bad = OrderStatus.valueOf(dto.status) // ✅ lookup plus an explicit fallback … - 15
for (d in downloads) if (d.done) downloads.remove(d)throws ConcurrentModificationException on a single thread — what is the Kotlin way to write it?EasyAn
ArrayList's iterator records a modification counter when it is created and checks it on everynext(), so removing through the list while a loop over it is open invalidates the iterator you are standing on.data class Download(val id: Long, val done: Boolean, val bytes: Long) val downloads = mutableListOf(Download(1, true, 10), Download(2, false, 0)) // ❌ the iterator sees modCount change under it for (d in downloads) if (d.done) downloads.remove(d) … - 16
orders.associateBy { it.customerId }returns 40 entries for 100 orders and nobody notices for a week — what happened, and what should have been written?MediumassociateBybuilds aMap, so two elements with the same key collide and the later one silently overwrites the earlier — the result can never be larger than the number of distinct keys.data class Order(val id: Long, val customerId: Long, val updatedAt: Instant, val total: Int) // ❌ silently keeps one order per customer — whichever came last in list order val byCustomer: Map<Long, Order> = orders.associateBy { it.customerId } // ✅ the key is not unique, so keep every value … - 17
A class nested in your Fragment does not compile until you add
inner, and then LeakCanary reports the Fragment retained — what does that keyword actually change?MediumA nested class in Kotlin holds no reference to the outer instance — it is what Java calls a static nested class — and
inneradds that hidden reference, which is both why it can suddenly read outer members and why it now keeps the outer object alive.class FeedFragment : Fragment() { private var binding: FeedBinding? = null private val viewModel: FeedViewModel by viewModels() // ❌ inner: a hidden this@FeedFragment field keeps the whole view tree alive inner class LeakyListener : EventBus.Listener { … - 18
A reviewer objects to
tasks.sortedBy { it.title }.sortedBy { it.due }as a two-key sort — is it actually wrong, and what would you write instead?MediumIt produces the correct order — Kotlin's sorts are stable, so the earlier sort survives inside equal keys — but it sorts the whole list twice, allocates two lists, and forces the reader to read the keys backwards.
data class Task(val title: String, val due: LocalDate, val priority: Int, val owner: String?) // works only by accident of stability: keys read backwards, list sorted twice val twice = tasks.sortedBy { it.title }.sortedBy { it.due } // ✅ one pass, keys in reading order … - 19
A class you cannot turn into a
data classfailsassertEqualseven though every field matches — what do you override, and what has to stay true afterwards?MediumAny.equalsis reference identity, so until you overrideequals— andhashCodein the same commit — two separately constructed objects are never equal no matter what they contain.// entity: identity is the id, and only the id class User(val id: Long, var displayName: String) { override fun equals(other: Any?): Boolean { if (this === other) return true // cheap fast path if (other == null || javaClass != other.javaClass) return false return id == (other as User).id // not displayName: it is mutable … - 20
When do you write
viewModel::onSubmitinstead of{ text -> viewModel.onSubmit(text) }, and what does that reference actually hold on to?MediumA callable reference is a function value pointing at an existing declaration: a bound one (
obj::method) captures the receiver at the moment the reference is created, an unbound one (Type::method) takes the receiver as its first parameter.class SubmitViewModel { fun onSubmit(text: String) { /* ... */ } } val vm = SubmitViewModel() val bound: (String) -> Unit = vm::onSubmit // receiver captured here and now …