skip to content

How can you compose behavior from multiple interfaces using `by`, and what are the design and equals/hashCode/identity caveats versus a single inheritance chain?

level: seniorimportance: nice to knowfreq 22%

answer

  1. delegate several interfaces to several objects
  2. Kotlin has no multiple class inheritance
  3. conflicting members need explicit override
  4. equals/hashCode/toString never auto-forwarded
  5. wrapper has its own identity

basics

~20 s

A class can delegate several interfaces to several different objects at once, mixing in behaviors. Unlike single inheritance you can combine many sources. But the wrapper is a new object, so identity and equals/hashCode are its own unless you forward them too.

solid answer

~50 s

Kotlin allows `class C(a: A, b: B) : A by a, B by b`, composing independent behaviors from multiple delegates — something single-class inheritance can't do (Kotlin has no multiple class inheritance). Each interface forwards to its own delegate; conflicting members from overlapping interfaces must be resolved by an explicit override. Caveats: the wrapper is a distinct object with its own identity; `equals`/`hashCode`/`toString` come from `Any` (or `data`) and are **not** auto-forwarded to a delegate, so `wrapper == delegate` is false unless you override. This matters for collections and caches. Also, two delegates can hold independent, possibly inconsistent state. Inheritance gives one cohesive object and one identity but limits you to a single base. Mixin-style delegation is powerful for cross-cutting concerns (logging, metrics, read-only views) but you must consciously manage identity, equality, and state coherence across delegates.

code

kotlin · 11 lines
kotlin
interface A { fun id(): Int }
interface B { fun id(): Int }
class C(private val a: A, private val b: B) : A by a, B by b {
    override fun id() = a.id()           // mandatory: resolve the clash
}

class RoView(private val inner: List<Int>) : List<Int> by inner
fun main() {
    val back = listOf(1, 2, 3)
    println(RoView(back) == back)        // false: equals not forwarded
}

go deeper

for a junior

Knows multiple interfaces can be delegated to different objects to mix behaviors.

for a middle

Can resolve member clashes with an explicit override and lists the two-delegate state risk.

for a senior

Explains why equals/hashCode/identity don't forward and the practical map-lookup/cache pitfalls.

for a principal

Designs mixin APIs deliberately — chooses single vs multi-delegate, mandates identity/equality contracts, and guards state coherence.

## Multi-interface composition Kotlin supports delegating several interfaces, each to its own object: ```kotlin interface Reader { fun read(): String } interface Writer { fun write(s: String) } class Channel(r: Reader, w: Writer) : Reader by r, Writer by w ``` `Channel` mixes a `Reader` and a `Writer` behavior from two unrelated delegates. Single inheritance can't do this: Kotlin permits only one base class (plus interfaces). This is the *mixin* use of delegation. ## Resolving conflicts If two delegated interfaces declare the same member, the compiler requires you to override it explicitly and choose: ```kotlin interface A { fun id(): Int } interface B { fun id(): Int } class C(a: A, b: B) : A by a, B by b { override fun id() = a.id() // mandatory disambiguation } ``` ## Identity and equals/hashCode caveats - `equals`, `hashCode`, and `toString` are members of `Any`, **not** part of your delegated interface, so **they are never auto-forwarded**. The wrapper uses `Any`'s reference identity (or `data class` generated ones). - Therefore `Channel(r, w) == r` is `false`, and the wrapper has its own hash. If client code keyed a map by the delegate and now passes the wrapper, lookups miss. If you need value-equality with the delegate, override `equals`/`hashCode` yourself. - `===` (referential identity) is obviously different too — the wrapper is a new object. ```kotlin class RoView(private val inner: MutableList<Int>) : List<Int> by inner val back = mutableListOf(1, 2, 3) val view = RoView(back) println(view == back) // false: List.equals not forwarded; identity used ``` (Compare to `back == listOf(1,2,3)` which is structural for real list impls.) ## State coherence Two delegates carry independent state. If a Reader-delegate and Writer-delegate must share a buffer, delegating to two separate objects can desync them; you'd delegate both to one object or coordinate explicitly. ## Versus inheritance | | Multi-`by` composition | Single inheritance | |---|---|---| | Sources of behavior | many interfaces/delegates | one base class | | Conflict resolution | explicit override required | linear, base wins | | Identity/equality | wrapper's own; forward manually | one object, natural | | Coupling | to interfaces | to base internals (fragile) | ## Recall Compose many delegates, but remember equals/hashCode/identity aren't part of any interface and won't forward.

  • Why doesn't a delegated `List` wrapper equal its backing list?
    equals/hashCode belong to Any, not to the List interface's forwarded members, so the compiler generates reference-identity equals for the wrapper unless you override it.
  • What happens if two delegated interfaces declare the same function?
    Compilation fails until you provide an explicit override choosing which delegate (or custom logic) to use.

A general contractor (wrapper) subcontracts plumbing and electrical to two firms — combined output, but the contractor's own name and signature, not the subs'.

saying these in an interview costs you the question

  • Assuming the wrapper equals its delegate by default
  • Thinking Kotlin supports multiple class inheritance
  • Forgetting that clashing members need explicit disambiguation
  • Believing toString/equals are forwarded through `by`

context