Interfaces #
An interface is a contract — it defines what a class must be able to do, without specifying how to do it. A class implementing an interface promises to provide implementations of all the abstract methods defined in it. Interfaces are the foundation of flexible, testable design: rather than depending on concrete implementations, good code depends on interfaces — so implementations can be swapped at any time without changing the code that depends on them. This article covers all aspects of interfaces in Kotlin, from basic declarations to important design patterns like multiple inheritance, diamond conflicts, and delegation.
Defining an Interface #
Interfaces are declared with the interface keyword. All methods in them are abstract by default (no implementation), except those explicitly given a body.
interface Vehicle {
// Abstract properties — must be implemented
val brand: String
val maxSpeedKmh: Int
// Abstract methods — must be implemented
fun startEngine()
fun stopEngine()
// Method with a default implementation — can be overridden, doesn't have to be
fun honk() {
println("Beep beep!")
}
fun info(): String = "$brand (max ${maxSpeedKmh}km/h)"
}
Three important things about interfaces: first, interfaces can’t store state (no backing fields for properties). Second, all interface members are public by default. Third, a class can implement multiple interfaces at once.
Implementing an Interface #
A class implementing an interface uses the : InterfaceName syntax — same as inheritance, but interfaces don’t need parentheses.
class Car(
override val brand: String,
override val maxSpeedKmh: Int,
val transmissionType: String
) : Vehicle {
private var engineRunning = false
override fun startEngine() {
if (!engineRunning) {
engineRunning = true
println("$brand: Engine started — vroom!")
} else {
println("$brand: Engine already running")
}
}
override fun stopEngine() {
if (engineRunning) {
engineRunning = false
println("$brand: Engine stopped")
}
}
// Doesn't override honk() — uses the interface's default implementation
// Doesn't override info() — uses the interface's default implementation
}
val avanza = Car("Toyota Avanza", 160, "Automatic")
avanza.startEngine() // Toyota Avanza: Engine started — vroom!
avanza.honk() // Beep beep!
println(avanza.info()) // Toyota Avanza (max 160km/h)
Properties in Interfaces #
Interfaces can declare properties, but can’t store their values (no backing field). There are two ways a class implements an interface property: with a regular property in the constructor, or with a custom getter.
interface Drawable {
val color: String
val lineThickness: Double
get() = 1.0 // default value via getter — can be overridden
fun draw()
fun calculate(): String
}
class Square(
override val color: String,
val side: Double
) : Drawable {
// lineThickness isn't overridden — uses the default 1.0
override fun draw() {
println("Drawing a square with side=${side}cm color=$color")
}
override fun calculate(): String {
val area = side * side
val perimeter = 4 * side
return "Area=${area}cm², Perimeter=${perimeter}cm"
}
}
class Circle(
override val color: String,
val radius: Double,
override val lineThickness: Double = 2.5 // override the default value
) : Drawable {
override fun draw() {
println("Drawing a circle r=${radius}cm color=$color line=${lineThickness}px")
}
override fun calculate(): String {
val area = Math.PI * radius * radius
val perimeter = 2 * Math.PI * radius
return "Area=${\"%.2f\".format(area)}cm², Perimeter=${\"%.2f\".format(perimeter)}cm"
}
}
val shapes: List<Drawable> = listOf(
Square("Red", 5.0),
Circle("Blue", 3.0)
)
shapes.forEach {
it.draw()
println(it.calculate())
println()
}
Interfaces as Types #
One of the most important uses of interfaces is as types — code that depends on an interface, not a concrete implementation. This makes code easy to test and easy to swap implementations.
interface Repository<T> {
fun findById(id: Long): T?
fun saveAll(items: List<T>): Boolean
fun delete(id: Long): Boolean
fun findAll(): List<T>
}
data class Product(val id: Long, val name: String, val price: Double)
// Implementation that stores data in memory (for testing)
class InMemoryProductRepository : Repository<Product> {
private val data = mutableMapOf<Long, Product>()
override fun findById(id: Long) = data[id]
override fun saveAll(items: List<Product>): Boolean {
items.forEach { data[it.id] = it }
return true
}
override fun delete(id: Long): Boolean = data.remove(id) != null
override fun findAll() = data.values.toList()
}
// Implementation that stores to a database (production)
class DatabaseProductRepository(private val connection: String) : Repository<Product> {
override fun findById(id: Long): Product? {
println("Query: SELECT * FROM produk WHERE id=$id via $connection")
return null // placeholder
}
override fun saveAll(items: List<Product>): Boolean {
println("INSERT ${items.size} products via $connection")
return true
}
override fun delete(id: Long): Boolean {
println("DELETE FROM produk WHERE id=$id via $connection")
return true
}
override fun findAll(): List<Product> {
println("SELECT * FROM produk via $connection")
return emptyList()
}
}
// Service that depends on the interface, not the implementation
class ProductService(private val repo: Repository<Product>) {
fun addProduct(product: Product) {
repo.saveAll(listOf(product))
}
fun findProduct(id: Long): Product? = repo.findById(id)
fun listProducts() = repo.findAll()
}
// In production — use the database
val prodService = ProductService(DatabaseProductRepository("jdbc:postgresql://..."))
// In tests — use in-memory
val testService = ProductService(InMemoryProductRepository())
testService.addProduct(Product(1, "Laptop", 15_000_000.0))
println(testService.listProducts())
Multiple Inheritance — Implementing Several Interfaces #
A class in Kotlin can only inherit from one class, but can implement many interfaces at once. This is how Kotlin achieves flexibility without the complexity of multiple class inheritance.
interface CanSpeak {
fun speak(message: String)
}
interface CanWalk {
fun walk(steps: Int)
fun currentPosition(): String = "Unknown"
}
interface CanSing {
fun sing(song: String)
fun volume(): Int = 5 // default volume 5
}
class Human(val name: String) : CanSpeak, CanWalk, CanSing {
private var position = 0
override fun speak(message: String) {
println("$name says: \"$message\"")
}
override fun walk(steps: Int) {
position += steps
println("$name walked $steps steps (position: $position)")
}
override fun currentPosition() = "Position $position" // overrides the default implementation
override fun sing(song: String) {
println("$name sings '$song' at volume ${volume()}")
}
// volume() isn't overridden — uses the default 5
}
val budi = Human("Budi")
budi.speak("Good morning!")
budi.walk(10)
budi.sing("Garuda Pancasila")
println(budi.currentPosition())
Diamond Conflict — Same Default Method from Two Interfaces #
When two interfaces provide default implementations for a method with the same name, the class implementing both must explicitly resolve the conflict.
interface A {
fun hello() = println("Hello from A")
fun greet() = println("Greeting from A")
}
interface B {
fun hello() = println("Hello from B")
fun greet() = println("Greeting from B")
}
// ANTI-PATTERN: not resolving the conflict — compilation error
// class C : A, B {
// // hello() from A or B? The compiler doesn't know
// }
// CORRECT: resolve the conflict explicitly
class C : A, B {
// Must override because there's a conflict
override fun hello() {
super<A>.hello() // call A's implementation
super<B>.hello() // call B's implementation
println("Hello from C itself")
}
// Pick one, or create a new implementation
override fun greet() {
super<B>.greet() // pick B only
}
}
val c = C()
c.hello()
// Hello from A
// Hello from B
// Hello from C itself
c.greet()
// Greeting from B
Interfaces with Generics #
Interfaces can be generic — defining contracts that work for various data types.
interface Transformation<I, O> {
fun transform(input: I): O
fun transformAll(inputs: List<I>): List<O> = inputs.map { transform(it) }
}
class StringToInt : Transformation<String, Int> {
override fun transform(input: String): Int = input.toIntOrNull() ?: 0
}
class IntToBinary : Transformation<Int, String> {
override fun transform(input: Int): String = Integer.toBinaryString(input)
}
val parser = StringToInt()
println(parser.transform("42")) // 42
println(parser.transform("not a number")) // 0
println(parser.transformAll(listOf("1", "2", "3", "abc"))) // [1, 2, 3, 0]
val binary = IntToBinary()
println(binary.transformAll(listOf(1, 5, 10, 255))) // [1, 101, 1010, 11111111]
The Delegation Pattern with by
#
Kotlin supports interface delegation natively using the by keyword. This lets a class delegate an interface’s implementation to another object without writing forwarding methods manually.
interface Storage {
fun save(key: String, value: String)
fun get(key: String): String?
fun delete(key: String)
}
// Concrete implementation
class MemoryStorage : Storage {
private val data = mutableMapOf<String, String>()
override fun save(key: String, value: String) { data[key] = value }
override fun get(key: String) = data[key]
override fun delete(key: String) { data.remove(key) }
}
// Delegation to MemoryStorage — no need to override every method
class LoggingStorage(private val delegate: Storage) : Storage by delegate {
// Only override methods that need extra behavior
override fun save(key: String, value: String) {
println("[LOG] Saving key='$key'")
delegate.save(key, value)
}
override fun delete(key: String) {
println("[LOG] Deleting key='$key'")
delegate.delete(key)
}
// get() is automatically delegated to the delegate — no need to write it
}
val memory = MemoryStorage()
val withLog = LoggingStorage(memory)
withLog.save("name", "Budi") // [LOG] Saving key='name'
withLog.save("age", "25") // [LOG] Saving key='age'
println(withLog.get("name")) // Budi — delegated to memory
withLog.delete("age") // [LOG] Deleting key='age'
Without by, you’d have to write a forwarding method for every method in the interface manually — very verbose for interfaces with many methods.
Interface vs Abstract Class #
This is a design decision that often confuses people. Here’s a quick guide:
flowchart TD
A{Need to store\nstate / backing field?} -- Yes --> B["Abstract Class"]
A -- No --> C{One class needs\nmore than one\nimplementation?}
C -- Yes --> D["Interface\n(multiple inheritance)"]
C -- No --> E{Defining\n'can do what'\nor 'is what'?}
E -- "Can do\n(ability/behavior)" --> F["Interface\nExample: CanFly, CanSwim"]
E -- "Is what\n(identity/type)" --> G["Abstract Class\nExample: Animal, Vehicle"]Comparison table:
| Aspect | Interface | Abstract Class |
|---|---|---|
| State | Can’t (no backing fields) | Can have properties with state |
| Multiple inheritance | Can implement many | Can only inherit one |
| Constructor | Doesn’t have one | Has a constructor |
| Member visibility | All public | Can be private, protected, etc. |
| Implementation keyword | : InterfaceName (no parentheses) | : ClassName() (with parentheses) |
| Best for | Abilities/behaviors, API contracts | “Is a type of” hierarchies |
// Interface — defines ABILITIES
interface CanExportCsv {
fun exportToCsv(): String
}
interface CanExportPdf {
fun exportToPdf(): ByteArray
}
// Abstract class — defines IDENTITY with shared state
abstract class BusinessDocument(
val title: String,
val createdAt: Long = System.currentTimeMillis()
) {
abstract fun buildContent(): String
fun metadata() = "Title: $title | Created: $createdAt"
}
// Concrete class — IS-A BusinessDocument, CAN export to CSV and PDF
class SalesReport(
title: String,
private val salesData: List<Pair<String, Double>>
) : BusinessDocument(title), CanExportCsv, CanExportPdf {
override fun buildContent(): String {
return salesData.joinToString("\n") { (product, value) ->
"$product: Rp${\"%,.0f\".format(value)}"
}
}
override fun exportToCsv(): String {
return "Product,Value\n" + salesData.joinToString("\n") { (p, v) -> "$p,$v" }
}
override fun exportToPdf(): ByteArray {
println("Generating PDF for: $title")
return ByteArray(0) // placeholder
}
}
Summary #
- Interfaces are capability contracts — they define what can be done, not who does it. Use interfaces to define behaviors that various unrelated classes can share.
- Default implementations reduce boilerplate — methods with bodies in interfaces let you add new methods without forcing all existing implementations to update.
- Interfaces can’t store state — properties in interfaces have no backing field. Implementations must provide their own storage, either via constructor or class properties.
- Multiple interfaces for multiple abilities — a class can implement many interfaces, enabling flexible capability composition without the complexity of multiple class inheritance.
- Diamond conflicts must be resolved — if two interfaces have default methods with the same name, the implementing class must override that method and define the desired behavior. Use
super<InterfaceName>.method()to call a specific implementation.- Interfaces as types enable loose coupling — code depending on interfaces rather than concrete implementations is far easier to test (can be mocked) and easier to swap implementations.
- Delegation with
by— usebyto delegate an interface’s implementation to another object without writing forwarding methods manually. Ideal for the Decorator pattern.- Interfaces for “can do”, abstract classes for “is a type of” — use interfaces when defining abilities/behaviors across hierarchies. Use abstract classes when defining identity with shared state.