RizTech Academy logo
RizTech Academy
Classes and ObjectsLesson 3 of 525 min

Interfaces and inheritance

Classes rarely stand alone. Sometimes several classes should share a common shape (every payment method can charge); sometimes one class is a specialised version of another (a SavingsAccount is an Account). Kotlin gives you interfaces and inheritance for these, with a crucial default that differs from most languages: classes are closed to inheritance unless you open them.

Interfaces — a shared contract

An interface declares what a type can do, without saying how:

interface PaymentMethod {
    fun charge(amountPaise: Int): Boolean       // no body — just the contract
    val displayName: String
}

Any class that implements the interface must provide those members:

class UpiPayment(val vpa: String) : PaymentMethod {
    override val displayName = "UPI ($vpa)"
    override fun charge(amountPaise: Int): Boolean {
        println("Charging ₹${amountPaise / 100} via UPI to $vpa")
        return true
    }
}

class CardPayment(val last4: String) : PaymentMethod {
    override val displayName = "Card ****$last4"
    override fun charge(amountPaise: Int): Boolean {
        println("Charging ₹${amountPaise / 100} to card ending $last4")
        return true
    }
}

The : PaymentMethod says "this class implements the interface", and override is required on each member that fulfils the contract — Kotlin makes you state your intent, so you cannot fulfil an interface by accident or misspell a method name and not notice.

The payoff is that code can work with any payment method through the interface, not knowing which concrete class it has — polymorphism:

fun checkout(method: PaymentMethod, amountPaise: Int) {
    println("Paying with ${method.displayName}")
    method.charge(amountPaise)
}

checkout(UpiPayment("kavita@okaxis"), 45000)
checkout(CardPayment("1234"), 45000)

checkout takes a PaymentMethod and calls charge — it works with UPI, card, or any future method you add, without changing. This is the single most valuable use of interfaces: program to the contract, not the concrete type, so new implementations slot in without touching existing code.

Interfaces can have default implementations

Unlike older Java, a Kotlin interface method can have a body — a default implementation that implementers inherit unless they override it:

interface PaymentMethod {
    fun charge(amountPaise: Int): Boolean
    val displayName: String
    fun describe(): String = "Payment via $displayName"   // default — free for all implementers
}

Every implementer gets describe() without writing it, and can override it if needed. Use defaults for behaviour that is the same across most implementers, keeping the classes lean.

Inheritance — and why classes are final by default

Inheritance lets one class extend another, reusing and specialising its behaviour. But there is a deliberate friction: Kotlin classes are final (closed) by default — you cannot inherit from one unless it is marked open:

open class Account(val id: String, protected var balancePaise: Int) {
    open fun describe(): String = "Account $id: ₹${balancePaise / 100}"
}

class SavingsAccount(id: String, balance: Int, val rate: Double) : Account(id, balance) {
    override fun describe(): String = "${super.describe()} (savings @ $rate%)"
}

Read the design decisions:

  • open class Account — the class must be open to be inherited; a plain class cannot be extended.
  • open fun describe() — a method must be open to be overridden; methods are also final by default.
  • : Account(id, balance) — the subclass calls the superclass constructor.
  • override — required to override, as with interfaces.
  • super.describe() — calls the parent's version, so SavingsAccount extends rather than replaces it.

Why closed by default? Because inheritance is a strong coupling that is easy to get wrong. Extending a class that was not designed to be extended is a classic source of fragile code — a change in the parent silently breaks the child. Kotlin makes you opt in to allowing inheritance, which forces the class author to think "is this safe to extend?" and design for it. It is the same philosophy as val and read-only collections: the safe, restrictive choice is the default.

Prefer interfaces and composition to inheritance

Here is the guidance that matters more than the mechanics. Reach for interfaces first, and inheritance sparingly. Deep inheritance hierarchies — A extends B extends C extends D — become rigid and hard to follow; a change high up ripples unpredictably downward. The modern, better-tested approach is:

  • Interfaces to share a contract (many classes, one shape).
  • Composition — a class holds another and delegates to it — to share behaviour, rather than inheriting it.

Kotlin even has first-class support for composition-over-inheritance with the by keyword (delegation), which the design-patterns module covers. The rule of thumb: use inheritance only for a genuine "is-a-specialised-kind-of" relationship where the subtype truly is the supertype; for everything else — sharing a contract, reusing behaviour — reach for an interface or composition. Junior developers over-use inheritance; experienced ones use it rarely and deliberately.

Check your work

What an interface declares. A contract — what a type can do — without (necessarily) how.

What override does, and why it is required. Marks a member as fulfilling a contract or replacing a parent's; required so you cannot do it by accident or by a misspelling.

What polymorphism buys you. Code works with any implementation through the interface, so new implementations slot in without changing existing code.

What a default implementation is. An interface method with a body, inherited unless overridden.

Why Kotlin classes are final by default. Inheritance is strong coupling that is easy to get wrong; opting in forces the author to design for extension.

What must be open. A class to be inherited, and a method to be overridden.

What super.method() does. Calls the parent's version, so a subclass can extend rather than replace.

The guidance on inheritance versus interfaces/composition. Prefer interfaces and composition; use inheritance only for a genuine "is-a-specialised-kind-of" relationship.

Practice

  1. Write a PaymentMethod interface and two implementing classes. Use override on each member.
  2. Write a checkout(method: PaymentMethod, ...) function and call it with both implementations. Note it does not know which concrete class it has.
  3. Add a default describe() to the interface and confirm both classes get it for free. Override it in one.
  4. Try to inherit from a plain (non-open) class and read the compiler error. Then mark it open.
  5. Write open class Account and a SavingsAccount subclass whose describe() calls super.describe().
  6. Try to override a method that is not open and read the error.
  7. Take a design you would instinctively model with inheritance and reconsider whether an interface or composition fits better. Write down which and why.

Official documentation

Next: sealed classes and exhaustive when — a Kotlin highlight.

Stuck on this lesson?

Being stuck is part of it — but being stuck alone for three days is not. Our internship programme pairs this curriculum with code review and one-to-one help from working developers, and it is free.

About the internship