December Code

Kotlin inheritance and interfaces interview questions, with answers

Kotlin keeps inheritance but makes it explicit. Classes and methods are closed unless marked open, overriding needs the override keyword, and sealed types let the compiler know every subtype of a hierarchy. Interviews probe those rules and the differences from Java: what an interface may contain, how a class chooses between two inherited implementations, and how type checks and casts work.

The answers below cover open and override, abstract classes and interfaces, sealed hierarchies, super calls, object expressions and casts, with code compiled with kotlinc 2.3 and run on the JVM. Then take the free Kotlin diagnostic — ten questions across every Kotlin topic in the bank — to see which of these you can explain but not yet predict.

The questions, with answers

  1. 1.What do open and override mean in Kotlin?

    In short: Classes and members are final by default; open allows a class to be subclassed or a member to be overridden, and override marks the overriding member.

    Kotlin reverses Java's default: a class cannot be extended, and a method cannot be overridden, unless it is declared open. A subclass then overrides with the override keyword, which is mandatory, so an accidental override of a similarly named method cannot happen. Calls through a base-class reference are dispatched on the object's runtime type, so s.name() below returns circle. An override is itself open unless marked final override. The final-by-default rule follows the guideline to design for inheritance or prohibit it; frameworks that need to subclass classes, such as Spring, use a compiler plugin that opens them.

    open class Shape {
        open fun name() = "shape"
    }
    
    class Circle : Shape() {
        override fun name() =
            "circle"
    }
    
    fun main() {
        val s: Shape = Circle()
        println(s.name())
        // circle
    }
  2. 2.What is the difference between an abstract class and an interface in Kotlin?

    In short: An interface declares behaviour, with default method bodies and abstract properties but no state; an abstract class can also hold state and constructor parameters, and a class extends only one.

    Kotlin interfaces can contain abstract members, methods with default implementations, and properties that are either abstract or computed by getters, but no stored state and no constructor. A class can implement any number of interfaces. An abstract class can have constructor parameters, stored properties and initialisation logic in addition to abstract members, and a class can extend only one class. The code combines both: Named supplies hi() for any type with a name, and Pet stores the name and leaves cry abstract for Cow to implement. Interfaces are the default choice for capabilities; abstract classes suit shared state and construction.

    interface Named {
        val name: String
        fun hi() = "hi $name"
    }
    
    abstract class Pet(
        override val name: String,
    ) : Named {
        abstract fun cry(): String
    }
    
    class Cow : Pet("cow") {
        override fun cry() = "moo"
    }
    
    fun main() {
        val c = Cow()
        println(c.hi())
        // hi cow
        println(c.cry())
        // moo
    }
  3. 3.What are sealed classes and interfaces in Kotlin?

    In short: A sealed type restricts its direct subtypes to the same module and package, so the compiler knows them all and a when over it can be exhaustive without else.

    sealed interface Ui below can only be implemented by types declared alongside it, here Loading and Ok. Because the set is closed, a when expression that handles every subtype is exhaustive and needs no else branch, and each is branch smart-casts, so u.n can be read directly. If a new subtype is added later, every when that does not handle it stops compiling, which is exactly what a state machine or a result type wants. Subtypes can be data classes, data objects for states without data, or ordinary classes. Sealed classes can also hold shared state and constructors, while sealed interfaces allow a subtype to belong to several hierarchies.

    sealed interface Ui
    data object Loading : Ui
    data class Ok(val n: Int) : Ui
    
    fun show(u: Ui) = when (u) {
        Loading -> "..."
        is Ok -> "got ${u.n}"
    }
    
    fun main() {
        println(show(Ok(3)))
        // got 3
    }
  4. 4.How do you call a superclass implementation in Kotlin?

    In short: Inside an override, super.method() calls the implementation inherited from the superclass, so the override can extend it rather than replace it.

    An overriding method often builds on the inherited behaviour: Shout.hi below takes the result of super.hi() and upper-cases it. super also reaches superclass properties, and a subclass's constructor calls the superclass constructor in its header, as in class Shout : Parent(). Inside an inner class, super@Outer.method() reaches the outer class's superclass. When a class inherits the same member from more than one supertype, a plain super call would be ambiguous, so it must name the supertype in angle brackets, super<A>.id().

    open class Parent {
        open fun hi() = "hello"
    }
    
    class Shout : Parent() {
        override fun hi() =
            super.hi().uppercase()
    }
    
    fun main() {
        println(Shout().hi())
        // HELLO
    }
  5. 5.What happens when a Kotlin class inherits the same method from two interfaces?

    In short: The class must override the method, and inside the override it can call each inherited version explicitly with super<Interface>.method().

    Interfaces can carry default implementations, so a class implementing two interfaces may inherit two different bodies for one method. Kotlin does not pick one: the class must override the method, or it does not compile. Inside the override, super<A>.id() and super<B>.id() call each interface's version, so the class can choose one, combine them as C does below, or ignore both. This is Kotlin's answer to the diamond problem for behaviour; state cannot conflict, because interfaces cannot hold it. Java's default methods follow the same rule, written A.super.id().

    interface A {
        fun id() = "A"
    }
    
    interface B {
        fun id() = "B"
    }
    
    class C : A, B {
        override fun id(): String {
            val a = super<A>.id()
            val b = super<B>.id()
            return a + b
        }
    }
    
    fun main() {
        println(C().id())
        // AB
    }
  6. 6.What is an object expression in Kotlin?

    In short: object : Type { ... } creates an instance of an anonymous class on the spot, which can implement interfaces or extend a class, like Java's anonymous inner classes.

    When an interface implementation is needed once, an object expression defines and instantiates it in one step, as the Clicker below. It can extend a class and implement several interfaces, override members, add its own properties, and capture local variables of the enclosing function. For an interface with a single abstract method declared as a fun interface, a lambda is shorter, and Java's single-method interfaces accept lambdas directly through SAM conversion. An object expression creates a new object each time it is evaluated, unlike an object declaration, which defines a singleton.

    interface Clicker {
        fun click(): String
    }
    
    fun main() {
        val c = object : Clicker {
            override fun click() =
                "clicked"
        }
        println(c.click())
        // clicked
    }
  7. 7.How do is, as and as? work in Kotlin?

    In short: is checks a value's type and smart-casts it; as casts and throws a ClassCastException on failure; as? casts and returns null on failure.

    x is String tests the runtime type, and in the code it guards, x is already treated as a String. When a cast is really needed, x as String converts the value and throws a ClassCastException if it is not a String. The safe cast, x as? Int, returns null instead of throwing, so it combines naturally with the Elvis operator: (x as? Int) ?: 0. In the code, x holds a String, so as? String succeeds and as? Int gives null. Because generic type arguments are erased on the JVM, is List<String> cannot be checked, except in inline functions with reified type parameters.

    val x: Any = "kt"
    val s = x as? String
    val n = x as? Int
    println("$s $n")
    // kt null

How the diagnostic asks it

One question from the Kotlin bank, exactly as a sitting would show it. The bank has 3 on inheritance & interfaces and 30 across Kotlin.

Inheritance & Interfaces · mediumKT-016

What does this Kotlin program print?

sealed interface Outcome
data class Ok(val v: Int) : Outcome
data class Err(val msg: String) : Outcome

fun show(r: Outcome) = when (r) {
    is Ok -> "ok ${r.v}"
    is Err -> "err ${r.msg}"
}

fun main() {
    println(show(Ok(1)) + " / " + show(Err("x")))
}
  1. 1It does not compile: when needs an else branch
  2. 2ok 1 / err xcorrect
  3. 3It does not compile: r must be cast before reading v
  4. 4ok 1 / null

A sealed interface's direct subtypes must all be declared in the same module and package, so the compiler knows the complete list: Ok and Err. A when expression over it that covers both is therefore exhaustive and needs no else branch. Inside each branch, the is check smart-casts r, so r.v and r.msg can be read without a cast. The output is ok 1 / err x. The else option describes a when over an ordinary open interface. The cast option ignores smart casts. Adding a third subtype later makes this when a compile error until it is handled, which is the point of sealed types.

Measure it

Reading answers tells you what’s true. A diagnostic tells you what you get wrong.

10 Kotlin questions across its topics, easy to hard, about fifteen minutes. You get a readiness figure with the arithmetic shown, the topics you missed named, and a practice set sized for today. Free: 1 diagnostic a month and 15 problems a day. No card.

What the readiness test measures · how the score is computed

By Harshit · updated