Interfaces
An interface in Kotlin defines a contract: a set of properties and functions that any implementing class promises to provide, without necessarily saying how. Unlike old-style Java interfaces, a Kotlin interface can also supply default method bodies and computed properties, so it doubles as a lightweight way to share behavior across otherwise unrelated classes. Because a class can implement any number of interfaces but extend only one class, interfaces are Kotlin’s primary tool for multiple inheritance of behavior. This lesson covers everything from basic syntax to diamond-conflict resolution and Kotlin’s special single-method fun interface for lambdas.
Overview: How Interfaces Work
An interface declares members that fall into two groups: abstract members, which have no body and must be supplied by every implementing class, and members with a default implementation, which are inherited automatically unless a class chooses to override them. This is a big upgrade over interfaces in older Java versions, where every method had to be abstract. A second, easy-to-miss rule: every member you declare in a Kotlin interface is implicitly open — it can always be overridden. That is the opposite of ordinary Kotlin classes, where methods and properties are final by default and must be marked open explicitly to allow overriding.
Interfaces cannot hold state. A property declared in an interface is either abstract (val name: String, no initializer, no backing field) or computed with a custom getter (val name: String get() = "Unknown"). What an interface can never do is write val name: String = "Unknown" directly, because that initializer would require a backing field, and interfaces have none. Every implementing class must supply the actual storage — either as a constructor property, a property with its own initializer, or by inheriting a computed getter from the interface.
Because a class can implement many interfaces at once, two interfaces can supply conflicting default implementations of the same function — the classic diamond problem. Kotlin does not pick a winner silently. If a class inherits two different implementations of the same member, the compiler forces you to override it yourself, and inside that override you can reach a specific parent’s version with the syntax super<InterfaceName>.member().
One more detail worth knowing: since Kotlin 1.4, interfaces are also allowed to contain private functions and private property getters. These are not part of the public contract — they exist purely as internal helpers that default implementations can call, which lets you avoid duplicating logic across several default methods. They still cannot hold mutable state; only the visibility changes, not the no-backing-field rule.
Functional Interfaces (SAM Conversion)
When an interface has exactly one abstract member, you can mark it fun interface. This enables SAM conversion (Single Abstract Method): instead of writing an anonymous object, you can pass a plain lambda anywhere that interface is expected, and Kotlin wraps it into an instance automatically.
fun interface Validator {
fun isValid(value: Int): Boolean
}
fun checkAll(values: List<Int>, validator: Validator): Boolean {
return values.all { validator.isValid(it) }
}
fun main() {
val allPositive = Validator { it > 0 }
println(checkAll(listOf(1, 2, 3), allPositive))
println(checkAll(listOf(1, -2, 3), allPositive))
}
Output:
true
false
Here Validator { it > 0 } is not calling a constructor — interfaces don’t have constructors — it is Kotlin converting the trailing lambda into an object implementing Validator, with isValid forwarding to the lambda body. This is exactly how listener-style APIs and callback parameters read so cleanly in idiomatic Kotlin.
| Feature | Interface | Abstract Class |
|---|---|---|
| Can hold state (backing fields) | No | Yes |
| Has a constructor | No | Yes |
| A class can have how many? | Many (implement several) | One (extend only one) |
| Members open by default | Always | Only if marked open |
| Default method bodies | Yes | Yes |
Syntax
The general shape of an interface and a class implementing it:
interface InterfaceName {
// Abstract property: no initializer, no backing field allowed
val abstractProperty: Int
// Property with a custom getter: allowed, computed on every access
val computedProperty: String
get() = "computed value"
// Abstract function: no body, must be overridden
fun abstractFunction(x: Int): String
// Function with a default implementation: inherited unless overridden
fun defaultFunction(): String {
return "default"
}
}
class ImplementingClass : InterfaceName {
override val abstractProperty: Int = 42
override fun abstractFunction(x: Int): String {
return "Got $x"
}
// defaultFunction() is inherited as-is unless overridden here
}
interface InterfaceName { ... }— declares the contract; no constructor parameters are allowed after the name.val abstractProperty: Int— a property every implementer must override with real storage or a getter.get() = ...— gives a property a default, stateless implementation.fun abstractFunction(...)— a method with no body; mandatory to override.fun defaultFunction() { ... }— a method with a body; inherited automatically, overriding is optional.class ImplementingClass : InterfaceName— a single colon is used for both extending a class and implementing interfaces; list several interfaces separated by commas.override— required on every member that satisfies an interface’s abstract member.
Examples
Example 1: Abstract Property Plus a Default Method
interface Greeter {
val greeting: String
fun greet(name: String): String {
return "$greeting, $name!"
}
}
class EnglishGreeter : Greeter {
override val greeting = "Hello"
}
fun main() {
val greeter: Greeter = EnglishGreeter()
println(greeter.greet("Kotlin"))
}
Output:
Hello, Kotlin!
greeting is abstract, so EnglishGreeter must supply it; greet already has a body in the interface, so EnglishGreeter gets it for free. Notice the variable is typed as Greeter, not EnglishGreeter — code that only needs the contract should depend on the interface type, not the concrete class.
Example 2: Resolving a Diamond With super<Type>
interface Flyer {
fun move(): String = "flies"
}
interface Swimmer {
fun move(): String = "swims"
}
class Duck : Flyer, Swimmer {
override fun move(): String {
return "${super<Flyer>.move()} and ${super<Swimmer>.move()}"
}
}
fun main() {
val duck = Duck()
println("A duck ${duck.move()}.")
}
Output:
A duck flies and swims.
Duck inherits two different bodies for move(), so the compiler refuses to guess which one you meant and requires an explicit override. Inside that override, super<Flyer>.move() and super<Swimmer>.move() reach each parent’s specific version by name, and the override is free to combine, pick one, or replace both entirely.
Example 3: Polymorphism Over a List of Interface Types
interface Shape {
val name: String
fun area(): Double
fun describe(): String {
return "$name has area ${"%.2f".format(area())}"
}
}
class Circle(private val radius: Double) : Shape {
override val name = "Circle"
override fun area(): Double = Math.PI * radius * radius
}
class Rectangle(private val width: Double, private val height: Double) : Shape {
override val name = "Rectangle"
override fun area(): Double = width * height
}
fun main() {
val shapes: List<Shape> = listOf(Circle(2.0), Rectangle(3.0, 4.0))
for (shape in shapes) {
println(shape.describe())
}
}
Output:
Circle has area 12.57
Rectangle has area 12.00
The list is typed List<Shape>, so the loop body only ever talks to the interface; it never needs to know whether it’s holding a Circle or a Rectangle. Each class supplies its own area(), but both get describe() from the interface’s default implementation for free — this is interface-based polymorphism, the same pattern you’d reach for in Java, just with less boilerplate.
How It Works Step by Step
When the compiler processes a class that implements one or more interfaces, it walks through several checks in order:
- 1. Collect required members. Every abstract property and function across all implemented interfaces goes on a checklist the class must satisfy.
- 2. Check for conflicts. If two interfaces provide different default bodies for the same signature, that member is added to the checklist too — even though both interfaces gave it a body, Kotlin won’t silently choose one.
- 3. Verify overrides. The compiler confirms the class (or one of its own superclasses) supplies an
overridefor everything on the checklist. Anything missing is a compile error, not a runtime failure. - 4. Compile to JVM default methods. Under the hood, a Kotlin interface with method bodies compiles to a JVM interface using
defaultmethods, the same mechanism Java 8+ uses. Dispatch is virtual: calling a function through an interface-typed reference looks up the actual implementation on the object’s real class at runtime. - 5. Property access goes through accessors. Because interface properties have no backing field, reading
shape.namealways invokes a getter (either the implementing class’s stored property getter or the interface’s computed one) — there is no direct field access to fall back to.
Common Mistakes
Mistake 1: Giving an Interface Property an Initializer
interface Named {
val name: String = "Unknown"
}
// Compiler error: "Property initializers are not allowed in interfaces"
An initializer implies a backing field, and interfaces cannot store state. If you want a default value, expose it through a computed getter instead, and let implementers override it with their own storage when they need to.
interface Named {
val name: String
get() = "Unknown"
}
class Person(override val name: String) : Named
class Anonymous : Named
fun main() {
val p = Person("Ava")
val a = Anonymous()
println(p.name)
println(a.name)
}
Output:
Ava
Unknown
Mistake 2: Ignoring a Diamond Conflict
interface A {
fun hello(): String = "Hello from A"
}
interface B {
fun hello(): String = "Hello from B"
}
class C : A, B
// Compiler error: Class 'C' must override public open fun hello()
// because it inherits multiple implementations of it
C inherits two different bodies for hello() and gives the compiler no way to know which one you want, so it refuses to pick for you. The fix is to override the member explicitly and choose (or combine) the parent implementations with super<Type>.
interface A {
fun hello(): String = "Hello from A"
}
interface B {
fun hello(): String = "Hello from B"
}
class C : A, B {
override fun hello(): String {
return "${super<A>.hello()} and ${super<B>.hello()}"
}
}
fun main() {
val c = C()
println(c.hello())
}
Output:
Hello from A and Hello from B
Mistake 3: Trying to Instantiate an Interface Directly
interface Greeter {
fun greet(): String
}
fun main() {
val g = Greeter()
println(g.greet())
}
// Compiler error: Interface Greeter does not have constructors
An interface is a contract, not a blueprint for an object — it has no constructor because it has no fields to initialize. You need either a real class that implements it, or, for one-off cases, an anonymous object expression.
interface Greeter {
fun greet(): String
}
fun main() {
val g = object : Greeter {
override fun greet(): String = "Hi there"
}
println(g.greet())
}
Output:
Hi there
Best Practices
- Depend on the interface type in variables, parameters, and return types (
List<Shape>, notList<Circle>) so callers stay decoupled from concrete implementations. - Use default method bodies to share behavior that is genuinely the same across implementers, but keep interfaces focused — a huge interface with many defaults is usually a sign it should be split into smaller ones.
- Reach for
fun interfacewhenever a type only needs a single abstract method; it lets callers pass a plain lambda instead of writing an anonymous object. - When a class implements two interfaces with the same default member, resolve it explicitly with
super<Type>.member()rather than deleting one of the interfaces just to make the error go away. - Remember interfaces cannot store mutable state; if a type genuinely needs shared, mutable fields plus behavior, an abstract class (or composition) may be a better fit than an interface.
- Prefer computed getters (
get() = ...) over exposing raw abstract properties when a interface can offer a sensible default, so implementers only override what actually differs.
Practice Exercises
- Exercise 1: Define an interface
Playablewith an abstractfun play(): Stringand a defaultfun stop(): Stringreturning"Stopped". Implement it in two classes, e.g.AudioTrackandVideoTrack, each overriding onlyplay(). Put both in aList<Playable>and printplay()andstop()for each. - Exercise 2: Create two interfaces,
NameableandAgeable, each with a defaultfun describe(): Stringreturning a different sentence. Implement both in a classRobot, resolve the conflict withsuper<Nameable>.describe()andsuper<Ageable>.describe(), and combine the two strings into one line. - Exercise 3: Write a
fun interface Transformerwith one abstract member,fun transform(x: Int): Int. Use SAM conversion to build a lambda that doubles a number, apply it tolistOf(1, 2, 3)with.map { transformer.transform(it) }, and print the result. Expected output:[2, 4, 6].
Summary
- An interface declares a contract of properties and functions; members are abstract by default and always implicitly
open. - Interface functions can carry a default body, and interface properties can carry a computed getter — but interfaces can never hold state (no backing fields, no initializers).
- A class implements interfaces after a colon, separated by commas, and can implement as many as it needs, unlike single-class inheritance.
- When two implemented interfaces provide conflicting default implementations, the compiler forces an explicit override, resolved with
super<InterfaceName>.member(). - An interface with exactly one abstract member can be marked
fun interfaceto enable SAM conversion, letting callers pass a lambda instead of an object. - Interfaces have no constructors; instantiate them via an implementing class or an anonymous
object : Interface { ... }expression.
