Kotlin Interfaces Carry More Than Java's Ever Could


For most of Java’s life an interface was a pure contract: method signatures, no bodies, no state. Java 8 loosened that with default methods. Kotlin started from the looser model — its interfaces can carry default implementations and declare properties — which makes them a sharper tool than the ones you may remember.

This post follows the class types and enums tour. Interfaces are the other half of how Kotlin models behavior.

Declaring and implementing

You declare an interface with interface, and a class adopts it with the same : syntax used for inheritance — there’s no separate implements keyword:

interface Greeter {
    fun greet(name: String): String
}

class Formal : Greeter {
    override fun greet(name: String) = "Good evening, $name."
}

override is mandatory, the same as when overriding a class member. The compiler won’t let you silently shadow something.

Default implementations

A method in an interface can have a body. Implementers inherit it for free and override only when they need something different:

interface Greeter {
    fun greet(name: String): String

    fun greetAll(names: List<String>): String =
        names.joinToString("\n") { greet(it) }   // built on greet()
}

Any Greeter gets greetAll without writing it. Notice the default method calls greet — an interface can build richer behavior on top of its own abstract members.

Interfaces can declare properties

This is the part Java still can’t do cleanly. An interface can require a property. It can’t hold a backing field, but it can demand one exists — or provide a value through a getter:

interface Named {
    val name: String                    // implementers must provide it
    val label: String                   // derived, no storage needed
        get() = "Name: $name"
}

class User(override val name: String) : Named

name becomes part of the contract; label is computed from it. The implementing class supplies name and inherits label.

Implementing several, and resolving clashes

A class can implement any number of interfaces. When two of them provide a default method with the same signature, the compiler stops and makes you choose — using super<Interface> to name which one you mean:

interface A { fun ping() = "A" }
interface B { fun ping() = "B" }

class C : A, B {
    override fun ping() = super<A>.ping() + super<B>.ping()   // "AB"
}

No silent winner, no diamond ambiguity — the conflict is a compile error until you resolve it explicitly.

Interface or abstract class?

Both can hold abstract members and concrete ones, so the line can blur. Two questions decide it:

  • Do you need state? An interface can’t hold a backing field; an abstract class can. If implementers must share stored data, use the class.
  • Do you need more than one? A class extends exactly one parent but implements many interfaces. If a type needs to be several things at once, those things must be interfaces.

The default is interface — it keeps types composable. Drop to an abstract class only when shared state or a single-rooted hierarchy actually demands it.

Interfaces can extend interfaces

An interface can build on others, accumulating their contracts. A class that implements the child has to satisfy everything up the chain:

interface Named { val name: String }
interface Aged { val age: Int }

interface Person : Named, Aged {
    fun describe() = "$name, $age"      // free to use both inherited members
}

Functional interfaces

When an interface has exactly one abstract method, marking it fun interface lets callers pass a lambda where an implementation is expected — Kotlin’s equivalent of a Java SAM type:

fun interface Validator {
    fun isValid(input: String): Boolean
}

val notBlank = Validator { it.isNotBlank() }   // lambda becomes a Validator
notBlank.isValid("hi")                          // true

Without fun, you’d write an anonymous object : Validator { ... }. But when the thing you’re passing is really just a function, a plain function type ((String) -> Boolean) is simpler still — reach for fun interface only when the name carries meaning or a Java caller needs a named type.

A worked example

Default methods and property contracts pay off when several types share behavior but differ in one detail. Here every shape computes describe the same way, supplying only its own area:

interface Shape {
    val area: Double
    fun describe(): String = "area = ${"%.1f".format(area)}"
}

class Circle(val r: Double) : Shape {
    override val area get() = Math.PI * r * r
}

class Square(val side: Double) : Shape {
    override val area get() = side * side
}

listOf(Circle(1.0), Square(2.0)).forEach { println(it.describe()) }

Each class provides one property; describe comes for free from the interface. That’s the whole appeal — shared behavior, with the differences pushed to the edges.

Final thoughts

Kotlin interfaces blur the old “contract vs. behavior” boundary: they hold method bodies and property contracts, so a lot of what used to need an abstract base class now fits in an interface that any class can mix in. State is the one thing they still can’t carry — and that, more than anything, is what tells you when to reach for a class instead.

Next, a feature that attacks the same problem from the other side — adding behavior to types you don’t even own: extension functions.

Practice: reinforce this with the companion workbook — short, click-to-reveal problems.