Kotlin OOP & Functional
Irbisa · cheatsheetSeptember 13, 2026

Kotlin OOP & Functional

Classes, data classes, sealed classes, objects, extension functions, scope functions

Junior Developer20 itemscompressed for a skim
  1. 01

    What are data classes in Kotlin and what does the compiler generate?

    Easy

    A data class exists 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
    …
  2. 02

    What are sealed classes / sealed interfaces in Kotlin?

    Medium

    A 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>
    }
    …
  3. 03

    What is the difference between class, object, and companion object in Kotlin?

    Medium

    object declares a singleton, companion object is a singleton bound to a class, and class is the ordinary instantiable declaration.

    // object — singleton
    object Logger {
      fun log(msg: String) = println("[LOG] \$msg")
    }
    Logger.log("hello")
    …
  4. 04

    What are extension functions and properties in Kotlin?

    Medium

    Extensions 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
    …
  5. 05

    What are scope functions (let, run, with, apply, also)?

    Medium

    Scope functions run a lambda in the context of an object, and they differ on two axes: how the object is referenced (it vs this) 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") }
    …
  6. 06

    Kotlin classes are final by default — how do open class, abstract class and interface differ?

    Medium

    Kotlin makes every class final by default, so open, abstract and interface are 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
    }
    …
  7. 07

    Which visibility modifiers does Kotlin have, and what does internal actually cover?

    Easy

    Kotlin declarations are public unless you say otherwise, and its extra level is internal, 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(
    …
  8. 08

    In what order do the primary constructor, property initializers, init blocks and a secondary constructor run?

    Easy

    Initialization 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" }
      }
    …
  9. 09

    You chain map and filter over a list. When does inserting asSequence() actually make it faster?

    Medium

    A 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. 10

    What does class LoggingRepo(private val inner: Repo) : Repo by inner generate, and where does that delegation stop helping?

    Medium

    Interface 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. 11

    When can you pass a lambda where an interface is expected in Kotlin?

    Medium

    A 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. 12

    How do you make +, [] and in work on a type of your own?

    Medium

    Kotlin maps each operator to one fixed function name, and marking that function operator is 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. 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?

    Easy

    reduce takes the first element as its seed, so an empty collection has nothing to start from and it throws UnsupportedOperationException: 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. 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?

    Easy

    valueOf is strict: any name that is not declared throws IllegalArgumentException, 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. 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?

    Easy

    An ArrayList's iterator records a modification counter when it is created and checks it on every next(), 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. 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?

    Medium

    associateBy builds a Map, 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. 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?

    Medium

    A nested class in Kotlin holds no reference to the outer instance — it is what Java calls a static nested class — and inner adds 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. 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?

    Medium

    It 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. 19

    A class you cannot turn into a data class fails assertEquals even though every field matches — what do you override, and what has to stay true afterwards?

    Medium

    Any.equals is reference identity, so until you override equals — and hashCode in 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. 20

    When do you write viewModel::onSubmit instead of { text -> viewModel.onSubmit(text) }, and what does that reference actually hold on to?

    Medium

    A 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
    …