Kotlin Interfaces Carry More Than Java's Ever Could
Interfaces with default method bodies and property contracts, generic interfaces, implementing several and resolving the clashes with super<T>, functional (SAM) interfaces, Java default-method interop, and choosing between an interface and an abstract class.
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 carry default implementations and declare properties — which makes them a sharper tool than the one you may remember. This chapter follows the class types and enums tour; interfaces are the other half of how Kotlin models behavior. Everything was run against Kotlin 2.4.10.
Declaring and implementing
You declare with interface and a class adopts it with the same : used for inheritance — there’s no implements keyword:
interface Greeter {
fun greet(name: String): String
}
class Formal : Greeter {
override fun greet(name: String) = "Good evening, $name."
}
override is mandatory, exactly as when overriding a class member — the compiler won’t let you silently shadow.
Default implementations
A method in an interface can have a body. Implementers inherit it 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 the abstract greet()
}
Any Greeter gets greetAll for free, and note it calls greet — an interface can build richer behavior on top of its own abstract members, the template-method pattern with no ceremony.
Interfaces can declare properties
The part Java still can’t do cleanly. An interface can require a property — it holds no 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
get() = "Name: $name"
}
class User(override val name: String) : Named
name becomes part of the contract; label is computed from it. The “no backing field” rule is the real boundary: an interface property is always either abstract (the implementer stores it) or computed (a getter). It can never hold state itself — which is the single fact that most often decides interface-versus-abstract-class below.
Generic interfaces
An interface can be parameterized, which is how the standard library defines Comparable<T>, Iterable<T>, and the rest:
interface Repository<T> {
fun findById(id: Long): T?
fun save(item: T)
}
class UserRepository : Repository<User> {
override fun findById(id: Long): User? = /* ... */ null
override fun save(item: User) { /* ... */ }
}
The generics chapter covers variance; here the point is that a contract can be generic and each implementer pins the type.
Implementing several, and resolving clashes
A class implements any number of interfaces. When two supply a default with the same signature, the compiler stops and makes you choose — super<Interface> names which one:
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.
Functional (SAM) 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, and it works with generics too:
fun interface Transform<T> { fun apply(x: T): T }
val doubler = Transform<Int> { it * 2 } // lambda becomes a Transform
doubler.apply(21) // 42
Without fun, you’d write an anonymous object : Transform<Int> { ... }. But when the thing you’re passing is really just a function, a plain function type ((Int) -> Int) is simpler still — reach for fun interface only when the name carries meaning, when you need multiple methods later, or when a Java caller expects a named type.
Interop: default methods and Java
One boundary detail, because it changed. When Java calls a Kotlin interface with default methods, those defaults are compiled as real Java default methods, so Java implementers inherit them cleanly — modern Kotlin no longer hides the body in a synthetic DefaultImpls class the way older versions did. If you’re publishing an interface for Java consumers, its defaults behave the way a Java engineer expects. The interop chapter covers the annotations that fine-tune this.
Interface or abstract class?
Both hold abstract and concrete members, so the line blurs. 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 must be several things at once, those things are 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.
A worked example
Default methods and property contracts pay off when several types share behavior but differ in one detail:
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 supplies one property; describe comes free from the interface. Shared behavior, differences pushed to the edges.
Final thoughts
Kotlin interfaces blur the old “contract vs. behavior” boundary: they hold method bodies and property contracts, so much of what used to need an abstract base class now fits in an interface any class can mix in. State is the one thing they still can’t carry, and that, more than anything, tells you when to reach for a class instead. Beyond that, fun interface bridges to lambdas, generics make contracts reusable, and super<T> keeps multiple inheritance of behavior unambiguous.
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.
Comments