---
title: 'Interfaces and Delegation'
source: 'https://academia.sh/en/courses/kotlin/interfaces-and-delegation'
course: 'The Kotlin Language'
language: en
updated: '2026-10-21T05:02:40+00:00'
license: 'CC BY-SA 4.0'
objectives:
    - 'Writes a property and a function with a default implementation in an interface'
    - 'Shows how the same default function of two interfaces is combined in a single class'
    - 'Delegates the implementation of an interface to another object with `by`'
    - 'Measures that delegation does not forward equality and what happens when it is combined with a data class'
---

# Interfaces and Delegation

Measures the interface, the default implementation, and delegation with `by` using the criterion that makes two values equal, and shows that delegation forwards the members of the interface but does not forward `equals`.

In the previous lesson we looked at whether an object is built as a single one or anew every time. In this lesson we take the behavior of the object out of the class: an interface writes a contract, and a class either implements that contract itself or delegates it to another object. The question comes down to delegation: when an object leaves its behavior to someone else, what makes two delegating objects equal?

An **interface** defines the members a type must conform to. In Kotlin an interface can carry not only signatures but also functions with bodies and read-only properties with bodies; these are called default implementations. When a class implements an interface, it has to write the members without bodies, and it overrides the ones with bodies if it wants to.

This lesson recalls the measurement axis once more. The measure from the Java curriculum that asks which type makes the decision (M08/K02) is not here: we look not at the type but at the reason two values count as equal. Being the same object is singleton, equality falling to identity is identity, the contents being equal is content, and only some of them being equal is partial content.

## The Measurement Setup

`Criterion` stays in the first block, in the same form, in every lesson. Each observation carries three values: `first`, `twin` (a value built separately with the same recipe), and `other` (a value that differs in only one respect). The rule sees the result, not the reason.

```kotlin
// Criterion.kt — classifies the criterion that makes two values equal
class Criterion(val label: String, first: Any?, twin: Any?, other: Any?) {
    val sameObject = first === twin
    val equalTwin = first == twin
    val equalOther = first == other

    val name: String = when {
        sameObject -> "singleton"
        !equalTwin -> "identity"
        !equalOther -> "content"
        else -> "partial-content"
    }

    fun report() {
        println("$label: same-object=$sameObject, twin=$equalTwin, other=$equalOther -> $name")
    }
}

fun summarize(checks: List<Criterion>) {
    for (name in listOf("singleton", "identity", "content", "partial-content")) {
        println("$name: ${checks.count { it.name == name }}")
    }
}
```
The model of the lesson itself stays in a second helper file. The `Printer` interface carries a read-only property and a default function; two classes implement it in different ways.

```kotlin
// Printers.kt — a property and a default implementation in an interface
interface Printer {
    val prefix: String
    val width: Int
        get() = prefix.length

    fun render(text: String): String = prefix + text
}

class PlainPrinter : Printer {
    override val prefix: String = "> "
}

class LoudPrinter : Printer {
    override val prefix: String = "# "
    override fun render(text: String): String = prefix + text.uppercase()
}
```

**TN26.** `Printer` has only one property without a body and one read-only property with a body; a property stored in the interface (one that carries a field) cannot be written, and this was not measured here. The blocks below compile the two `Printer` implementations and the wrappers written with `by` together with this helper.

## An Interface Member Can Carry a Body

The block below uses the two classes side by side. `PlainPrinter` wrote only the property without a body; `render` and `width` took the bodies in the interface. `LoudPrinter`, on the other hand, redefined `render`. The last line measures two `PlainPrinter` objects.

```kotlin
// Main.kt — interface default and override
fun main() {
    val plain = PlainPrinter()
    val loud = LoudPrinter()
    println(plain.render("ada"))
    println(loud.render("ada"))
    println(plain.width)
    val printers: List<Printer> = listOf(plain, loud)
    println(printers.map { it.render("x") })
    Criterion("two-defaults", PlainPrinter(), PlainPrinter(), loud).report()
}
```

```
> ada
# ADA
2
[> x, # X]
two-defaults: same-object=false, twin=false, other=false -> identity
```

`plain` used the `render` body of the interface and `loud` used its own body; from the `printers` list both were called under the same `Printer` name, and each gave its own result. `width` was defined only in the interface and gave the length of `prefix`. In the last line, two separate `PlainPrinter` objects came out `identity`: because the class does not redefine `equals`, equality fell to identity, and the two instances were not equal even though they use the same interface body. A default implementation shares behavior; it does not share equality.

## Conflicting Defaults

If a class implements two interfaces and both carry a function with a body and the same signature, the compiler cannot know which one to choose. The class has to redefine the function itself, and inside the body each one can be reached separately with `super<Interface>`.

```kotlin
// Main.kt — the same default of two interfaces combined in a single class
interface Reader {
    fun describe(): String = "reader"
}

interface Writer {
    fun describe(): String = "writer"
}

class Both : Reader, Writer {
    override fun describe(): String = super<Reader>.describe() + "+" + super<Writer>.describe()
}

fun main() {
    println(Both().describe())
    println(Both().let { it as Reader }.describe())
}
```

```
reader+writer
reader+writer
```

`Both` combined the two sources. The second line matters: when the `Both` object was seen as a `Reader`, the same combined body ran, because the choice was made by the function the object itself redefined, not by the type of the variable.

**TN27.** In this block we did not separately verify that a class that does not resolve the conflict fails to compile; only the resolved form was measured.

## Delegation with by

**Delegation** is leaving the implementation of an interface to another object. In Kotlin this is written in the form `: Printer by inner`: for every member of the interface, the compiler generates a function that forwards the call to `inner`. The object that forwards is called the delegating object, and the object the call goes to is called the **delegate**.

```kotlin
// Main.kt — delegation with by forwards the members of the interface
class Labeled(private val inner: Printer) : Printer by inner

class Quiet(private val inner: Printer) : Printer by inner {
    override fun render(text: String): String = inner.render(text).lowercase()
}

fun main() {
    val loud = LoudPrinter()
    val labeled = Labeled(loud)
    println(labeled.render("ada"))
    println(labeled.prefix)
    println(labeled.width)
    println(Quiet(loud).render("ADA"))
    val checks = listOf(
        Criterion("same-delegate", Labeled(loud), Labeled(loud), Labeled(PlainPrinter())),
        Criterion("overridden", Quiet(loud), Quiet(loud), Quiet(PlainPrinter())),
    )
    for (check in checks) check.report()
    summarize(checks)
}
```

```
# ADA
#
2
# ada
same-delegate: same-object=false, twin=false, other=false -> identity
overridden: same-object=false, twin=false, other=false -> identity
singleton: 0
identity: 2
content: 0
partial-content: 0
```

`Labeled` wrote no members; `render`, `prefix`, and `width` still worked: all three went to `loud`. That the `render` result is in capital letters shows that the call went not to the default body of `Printer` but to the body redefined by `LoudPrinter`. `Quiet`, on the other hand, redefined one member itself and delegated the rest.

Two `Labeled` objects wrapping the same delegate and two `Quiet` objects wrapping the same delegate both came out `identity`. Even though the two delegating objects are bound to the same `loud`, they were not counted as equal.

**TN28.** The expression after `by` does not have to be a constructor parameter; here we measured only the form that shows the parameter. Whether delegation also forwards a member that is not in the interface was not measured.

## The Functional Interface, the Object Expression, and the Delegate Expression

An interface is not implemented only by classes. An interface with a single abstract function can be built with a lambda if it is marked with `fun interface`. An interface can also be implemented by a nameless object expression. Third, the expression after `by` does not have to be a constructor parameter; it can be any expression. We measure these three in a single block.

`Shout` is built with two lambdas: the bodies of the two are the same. `anon` implements `Printer` with an object expression. The `Cached` class builds its delegate with the `makePrinter()` call, and the `made` counter counts how many times this call ran.

```kotlin
// Main.kt — fun interface, object expression, and the delegate expression evaluated once
fun interface Shout {
    fun apply(text: String): String
}

var made = 0

fun makePrinter(): Printer {
    made += 1
    return PlainPrinter()
}

class Cached : Printer by makePrinter()

fun main() {
    val first = Shout { it.uppercase() }
    val second = Shout { it.uppercase() }
    println(first.apply("ada"))
    val anon = object : Printer {
        override val prefix: String = "~ "
    }
    println(anon.render("x"))
    val one = Cached()
    println(one.render("a"))
    println(one.render("b"))
    println(made)
    Cached()
    println(made)
    val checks = listOf(
        Criterion("functional-interface", first, second, Shout { it }),
        Criterion("object-expression", anon, anon, PlainPrinter()),
    )
    for (check in checks) check.report()
}
```

```
ADA
~ x
> a
> b
1
2
functional-interface: same-object=false, twin=false, other=false -> identity
object-expression: same-object=true, twin=true, other=false -> singleton
```

The first result of the block is that two lambdas with the same body are not counted as equal: the `functional-interface` line came out `identity`. The bodies being the same as text did not produce equality; each lambda expression built its own object, and the functional interface does not redefine `equals`. Implementing the same interface with a class and implementing it with a lambda do not part ways as far as equality goes.

The second result is for the object expression. The `object-expression` line came out `singleton`, because `anon` is a single variable and the same value was given to the measure twice. The distinction we saw in the previous lesson holds here too: an expression builds a new object every time it is evaluated, but an object built once and put into a variable stays single. The `~ ` prefix in the output of `anon.render("x")` showed that the default `render` body in the interface is used in the object expression too.

The third result is for the `by` expression. `made` printed `1` after the first `Cached()` call and `2` after the second. The delegate expression is evaluated once while each `Cached` instance is being built: two instances carry two separate `PlainPrinter` objects. The two `render` calls of the same instance did not rebuild the delegate; `made` increased only in the constructor. This says that delegation is not a method call but a field stored in the constructor: the delegate is built once per instance and then forwarded to.

**TN29.** Whether a lambda capturing or not capturing an outer variable changes equality was not measured; neither lambda captured anything.

## The Equality Rule from the Measurements

Every line we measured in this lesson asked where a behavior comes from and then asked where equality would come from. An interface shares behavior but carries no state and writes no `equals`; `by` forwards behavior to a delegate but does not forward `equals`; `fun interface` turns a lambda into an interface value but does not count two identical lambdas as equal. The common result of the three setups is that equality is determined not by the sharing of behavior but by the class's own `equals` definition.

A common mistake is to read delegation like inheritance. In inheritance, a subclass inherits the `equals` of the superclass; a class that delegates with `by` forwards the members of the interface, but `equals` is not forwarded, because it is not a member defined in the interface. This is why two `Labeled` or two `Quiet` instances wrapping the same delegate, did not come out equal, even though their behaviors are exactly the same. If equality is needed, the class writes it itself or is declared as a `data class`; delegation does not do this job.

The delegate expression being evaluated once per instance gives a second practical conclusion. The `made` counter printed `2` for two instances; that is, the delegate is a field and stays the same for the lifetime of the instance. If state is kept inside a delegate, for example a counter or a cache, that state belongs to the delegating instance and is not shared between two instances.

The same lesson holds for interface values built with a lambda: even if two bodies carry the same text, they are two separate values, and the result comes out `identity`. Expecting equality from behavior turned out wrong in every setup we measured in this lesson. Thinking of behavior and equality separately keeps you from running into an unexpected result in a collection or in a `==` comparison.

## Predicting Before Measuring

Because the criterion of this lesson is `identity`, the prediction is easy, but saying why each line gave `identity` is more instructive. In an interface implemented by a class, equality depends on the `equals` of the class; if the class did not write it, the identity comparison of `Object` applies. `by` adds nothing to this table, because the forwarded members are the ones defined in the interface. In a class declared as a data class, `equals` is generated by the compiler and looks at the content; this lesson showed it as a comparison.

There is a prediction trap in the functional interface line. The two lambdas carry the same text and are turned into the same interface; it seems natural to think "they do the same job, so they are equal". The measurement gave `identity`. In the object expression line, on the other hand, the same variable was given twice and the result was `singleton`; a new object is built while the expression is evaluated, but an object put into a variable stays single from then on. The difference between these two lines is the difference between "two expressions" and "one expression, two reads".

In the delegate expression, what is to be predicted is a number. For a program that builds two `Cached` objects, the value of `made` will be `2`, because the delegate is evaluated once in the constructor. A person who expects this number to be `1` or as many as the number of calls has misunderstood whether delegation is a field or an expression recalculated on every call.

## The Bounding Measurement

Why are two delegating objects not equal? Because `by` forwards only the members of the interface; `equals`, `hashCode`, and `toString` are not members of the interface but members of `Any`. We verified this first: the fragment below did not compile on Kotlin 2.4.21, which is why it stays without output. It is also forbidden for an interface to give a default implementation for the `equals` of `Any`.

```kotlin
interface Same {
    override fun equals(other: Any?): Boolean = true
}
```

Since delegation does not forward `equals`, the only way to change equality is for the class itself to redefine it. A data class does this automatically. The block below measures an ordinary class and a data class wrapping the same delegate side by side; the third line compares the delegating object with its delegate.

```kotlin
// Main.kt — delegation does not forward equals, a data class generates its own
class Wrapped(val inner: Printer) : Printer by inner
data class Tagged(val inner: Printer) : Printer by inner

fun main() {
    val loud = LoudPrinter()
    val checks = listOf(
        Criterion("class-delegation", Wrapped(loud), Wrapped(loud), Wrapped(PlainPrinter())),
        Criterion("data-class-delegation", Tagged(loud), Tagged(loud), Tagged(PlainPrinter())),
        Criterion("delegator-and-delegate", Wrapped(loud), loud, PlainPrinter()),
    )
    for (check in checks) check.report()
    summarize(checks)
}
```

```
class-delegation: same-object=false, twin=false, other=false -> identity
data-class-delegation: same-object=false, twin=true, other=false -> content
delegator-and-delegate: same-object=false, twin=false, other=false -> identity
singleton: 0
identity: 2
content: 1
partial-content: 0
```

The criterion came out `identity` in the ordinary class and `content` in the data class. Because the data class generates its own `equals` and compares the `inner` in the primary constructor, two instances wrapping the same delegate became equal; the one wrapping a different delegate did not. The third line shows the limit of delegation openly: `Wrapped(loud)` and `loud` are not equal to each other. The delegating object behaves like its delegate but is not its delegate.

## Summary

- An interface writes the members a type must conform to; it can carry functions with bodies and read-only properties with bodies.
- If two interfaces bring the same default function, the class redefines it and reaches the two separately with `super<Interface>`.
- `by` forwards the members of the interface to the delegate; the bodies the delegate redefined itself are preserved.
- Delegation does not forward `equals`, and an interface cannot give a default implementation for `equals`; two ordinary objects wrapping the same delegate are not equal, while two data classes are equal.
- This lesson's criterion: **identity**. Even when behavior is shared, equality falls to identity unless the class redefines its `equals`.

## Next Step

Delegation moved behavior to another object inside the class. The next lesson adds behavior from outside the class: extension functions let you write a new function for a type whose source you do not touch. We will measure whether an extension can take the place of a member of the type itself, especially over `equals`.
