Kotlin Interfaces and Abstract Classes

Interfaces and abstract classes are two tools Kotlin gives you to define contracts — rules that a class must follow. Both enforce structure in your code, but they work differently and suit different situations.

Interfaces — Defining a Contract

An interface defines what a class can do without specifying how it does it. Think of an interface as a job description — it lists the required skills, but each person hired fulfills those skills in their own way.

interface Printable {
    fun print()        // No body — must be implemented
    fun preview() {
        println("Previewing...")   // Has a default body — optional to override
    }
}

Implementing an Interface

class Document(val title: String) : Printable {
    override fun print() {
        println("Printing document: $title")
    }
}

class Photo(val filename: String) : Printable {
    override fun print() {
        println("Printing photo: $filename")
    }

    override fun preview() {
        println("Showing photo preview of $filename")
    }
}

val doc = Document("Report.pdf")
doc.print()     // Printing document: Report.pdf
doc.preview()   // Previewing... (default used)

val photo = Photo("sunset.jpg")
photo.print()   // Printing photo: sunset.jpg
photo.preview() // Showing photo preview of sunset.jpg

Multiple Interfaces

A class can implement multiple interfaces. This is how Kotlin achieves multiple inheritance of behavior — unlike Java's class-based limitation.

interface Flyable {
    fun fly() { println("Flying...") }
}

interface Swimmable {
    fun swim() { println("Swimming...") }
}

class Duck : Flyable, Swimmable {
    override fun fly() { println("Duck flying low.") }
    // swim() uses default implementation
}

val duck = Duck()
duck.fly()    // Duck flying low.
duck.swim()   // Swimming...

Diagram — Multiple Interfaces

[Flyable]     [Swimmable]
  fly()         swim()
     \           /
      \         /
       [Duck]
       fly()     ← overridden
       swim()    ← default from Swimmable

Interface with Properties

interface Identifiable {
    val id: String        // Abstract property — no backing field
    val label: String
        get() = "ID: $id" // Property with default getter
}

class User(override val id: String, val name: String) : Identifiable

val user = User("U001", "Rahul")
println(user.id)     // U001
println(user.label)  // ID: U001

Abstract Classes — Partial Implementation

An abstract class can have both abstract members (no body, must be overridden) and concrete members (full body, ready to use). It bridges the gap between a regular class and an interface.

abstract class Report(val title: String) {
    abstract fun generateContent(): String   // subclass must implement

    fun save() {                             // shared implementation
        val content = generateContent()
        println("Saving report: $title")
        println(content)
    }
}

class SalesReport(title: String, val totalSales: Double) : Report(title) {
    override fun generateContent() = "Total Sales: ₹$totalSales"
}

class InventoryReport(title: String, val itemCount: Int) : Report(title) {
    override fun generateContent() = "Items in stock: $itemCount"
}

SalesReport("Q3 Sales", 450000.0).save()
// Saving report: Q3 Sales
// Total Sales: ₹450000.0

Interface vs Abstract Class — When to Use Which

Feature                          | Interface  | Abstract Class
---------------------------------|------------|----------------
Extend multiple                  | Yes        | No (single only)
Constructor parameters           | No         | Yes
Store state (fields)             | No         | Yes
Default method bodies            | Yes        | Yes
Can be instantiated directly     | No         | No
Best for                         | Shared     | Shared base
                                 | behavior   | with state

Practical Rule

Question: "Does this represent what a class IS or what it CAN DO?"

IS  → Abstract Class   (e.g., Employee is a type of person with a name)
CAN DO → Interface     (e.g., Printable, Clickable, Shareable)

Resolving Diamond Conflict

When two interfaces share the same method name, the implementing class must override the method and explicitly call the desired interface version.

interface A {
    fun greet() = println("Hello from A")
}

interface B {
    fun greet() = println("Hello from B")
}

class C : A, B {
    override fun greet() {
        super.greet()   // call A's version
        super.greet()   // call B's version
    }
}

C().greet()
// Hello from A
// Hello from B

Leave a Comment

Your email address will not be published. Required fields are marked *