1. Introduction
When you’re just starting to write code, it can seem like one class is “one role.” But reality quickly proves otherwise. The same object can simultaneously be “something that can print to the console,” “something that can serialize to CSV,” “something that can be logged,” “something that can be validated.” And if you try to dump everything into one interface EverythingEverywhereAllAtOnce, you won’t get an architecture—you’ll get a buffet of methods.
The key idea: it’s convenient to keep interfaces narrow and topic-focused. Then a class can implement several small contracts at once. Kotlin allows this: class X : A, B, C. The problem appears not because of multiplicity itself, but because contracts sometimes overlap in names and signatures. And that’s where the compiler becomes your strict editor: “In this scene two actors said the line render() at the same time—who do we listen to?”
Kotlin’s basic rule: ambiguity is forbidden
This rule is worth learning literally like a road sign.
If a class inherits multiple implementations of the same member (a function or a property) from its direct supertypes, Kotlin requires you to write override and define the final behavior yourself.
This applies both to the case of “two default implementations,” and to the case of “in one interface there’s a default, and in another there’s only a declaration (abstract).” Kotlin documentation phrases it like this: with multiple inheritance of implementations, you must explicitly resolve the ambiguity, and to choose a specific implementation you use a qualified super<...>.
An important mindset: the compiler isn’t being petty. It protects you (and future you) from the magic of “let it pick something somehow.” In languages where this gets “resolved automatically,” legendary bugs show up—ones that live exactly until the first release.
2. Conflict of default methods and super<...>
Conflict of two default methods
Imagine two interfaces—both provide a default implementation of the same method ping():
interface A {
fun ping(): String = "A"
}
interface B {
fun ping(): String = "B"
}
Now we want to make a class that implements both:
class C : A, B // <- questions start here
From Kotlin’s perspective this is: “Which ping() are you inheriting?” If the compiler silently picked one, you’d later spend a long time explaining to yourself why it “sometimes prints A.” So Kotlin will require you to:
- explicitly write override fun ping(),
- and inside (if needed) choose which implementation to call.
And this is where today’s hero appears: super<InterfaceName>.method().
super<...>: how to say “take the implementation from exactly here”
super without qualification works when inheritance is unambiguous (for example, a normal class inherits from one base class and there’s only one super). But when we have multiple sources of the same method, Kotlin requires a qualifier in angle brackets: super<A>.ping().
Example: “glue together” the behavior of two interfaces:
interface A {
fun ping(): String = "A"
}
interface B {
fun ping(): String = "B"
}
class C : A, B {
override fun ping(): String {
return super<A>.ping() + super<B>.ping()
}
}
fun main() {
val c = C()
println(c.ping()) // AB
}
From a code-reading standpoint this is convenient: you open the method and immediately see “the final behavior is a combination of A and B.” No shamanism.
3. Other types of conflicts
Default + abstract method: “there seems to be an implementation, but override is still required”
This is the most common “newbie trap.”
Imagine that one interface provides a default implementation, and the second only requires the method (without a body):
interface WithDefault {
fun value(): Int = 10
}
interface MustImplement {
fun value(): Int
}
It seems like: “But we already have a ready-made implementation—what does the compiler want?” Kotlin looks at it differently: both interfaces declared the same member, so in the class it must be unambiguously fixed what happens.
You write:
class D : WithDefault, MustImplement {
override fun value(): Int = super<WithDefault>.value()
}
fun main() {
val d = D()
println(d.value()) // 10
}
Why is this better? Because MustImplement is a contract: “someone must give meaning to value().” If you “silently” get the implementation from WithDefault, a person reading the code may not notice that the class satisfies MustImplement not with its own logic, but by “picking up” someone else’s. Kotlin forces this to be explicit: either you write your own implementation, or you say “yes, I’m taking the default from WithDefault.”
Conflict between a class and an interface
The “diamond” also happens like this: there is a base class (with an implementation), and there is an interface (also with an implementation of the same method). Kotlin treats this as two implementations from two sources—so override is mandatory.
Mini example:
open class BaseLogger {
open fun tag(): String = "BASE"
}
interface UiTaggable {
fun tag(): String = "UI"
}
class ScreenLogger : BaseLogger(), UiTaggable {
override fun tag(): String {
return super<BaseLogger>.tag() + "-" + super<UiTaggable>.tag()
}
}
fun main() {
val logger = ScreenLogger()
println(logger.tag()) // BASE-UI
}
A useful habit: when you see class X : SomeClass(), A, B, keep in mind that conflicts are possible not only between A and B, but also between SomeClass and A/B.
Property conflicts: same logic, just in the form of val
Conflicts are usually discussed with functions, but properties behave similarly, because a property is essentially get() (and sometimes set()).
Imagine:
interface LeftLabel {
val label: String get() = "LEFT"
}
interface RightLabel {
val label: String get() = "RIGHT"
}
class BothLabels : LeftLabel, RightLabel {
override val label: String
get() = super<LeftLabel>.label + "+" + super<RightLabel>.label
}
fun main() {
val x = BothLabels()
println(x.label) // LEFT+RIGHT
}
super<LeftLabel>.label is essentially calling the getter from a specific interface. And again: the conflict is resolved with an override and an explicit choice of source.
4. Practical example: formatting expenses in different styles
“Human-readable” and CSV: two contracts, one conflict
Let’s tie the idea to a practical console application (say, an expense tracker). By this point in the course we already have data models (for example, data class Expense) and we periodically print them to the console. Over time it’s almost inevitable that you want to output the same entity in different formats: “human-readable” and “in CSV.”
Let’s make two contract interfaces: one for “human” text, another for CSV. And we’ll intentionally create a conflict: both will provide a default method formatTitle(...), but with different rules.
data class Expense(
val title: String,
val amount: Int
)
interface HumanFormat {
fun formatTitle(title: String): String =
title.trim().replaceFirstChar { it.uppercase() }
}
interface CsvFormat {
fun formatTitle(title: String): String =
"\"" + title.trim().replace("\"", "\"\"") + "\""
}
Now we want a class that “can do both” (for example, later we’ll choose the format depending on the command mode):
class ExpenseTitleFormatter : HumanFormat, CsvFormat {
override fun formatTitle(title: String): String {
// Choose the rule "for humans"
return super<HumanFormat>.formatTitle(title)
}
}
fun main() {
val f = ExpenseTitleFormatter()
println(f.formatTitle(" coffee ")) // Coffee
}
What matters in this example: a conflict does not mean “you did something wrong.” A conflict means “you have two reasonable formatting rules, but in this specific class you must make a decision.” And Kotlin forces you to lock that decision into code, not just keep it in your head.
If instead you need the CSV rule—change one line to super<CsvFormat>.formatTitle(title).
Combining behaviors: when super calls become a new rule
Sometimes the right answer to a conflict is not to choose one implementation, but to combine them and add a clear rule. For example, a human-readable title, but with a note that this is an “export string.”
interface HumanFormat {
fun formatTitle(title: String): String =
title.trim().replaceFirstChar { it.uppercase() }
}
interface CsvFormat {
fun formatTitle(title: String): String =
"\"" + title.trim().replace("\"", "\"\"") + "\""
}
class MixedTitleFormatter : HumanFormat, CsvFormat {
override fun formatTitle(title: String): String {
val human = super<HumanFormat>.formatTitle(title)
val csv = super<CsvFormat>.formatTitle(title)
return "$human (csv=$csv)"
}
}
fun main() {
val f = MixedTitleFormatter()
println(f.formatTitle(" big \"sale\" ")) // Big "sale" (csv="big ""sale""")
}
Note: we intentionally create two intermediate val variables. It’s slightly longer than “one line,” but noticeably more readable for both a beginner and a tired adult developer on a Friday evening.
5. Cheat sheet: when you need override and what to write inside
When you run into conflicts for the first time, your brain wants a simple cheat sheet. Let’s make a small table. It won’t replace understanding, but it will help you not panic.
| Situation | What happens | What to do |
|---|---|---|
| Two interfaces provide a default for one method | Two implementations → ambiguity | override + choose/compose via super<A>/super<B> |
| One interface has a default, the other declares it abstractly | Kotlin requires explicitness | override + either your own logic, or super<WithDefault> |
| A base class and an interface both implement a method | Two implementations from different supertypes | override + (optional) super<Base> and/or super<Interface> |
| Conflict in a property (val) with a getter | Essentially a conflict of getters | override val + choose/compose via super<...>.prop |
6. How to read such override in someone else’s code and not be afraid
When you see something like this in a project:
override fun foo() {
super<A>.foo()
super<B>.foo()
}
don’t treat it as “complexity for the sake of complexity.” It almost always carries one of three meanings.
The first meaning is “both behaviors are important; run both.” This often shows up in logging, notifications, metrics collection, handler chains. The second meaning is “we choose one implementation, but we want to record the choice explicitly.” Then there will be a single super<...> inside. The third meaning is “we don’t trust defaults and we fully write our own implementation,” and then super<...> may not appear at all.
A useful habit: if you see an interface conflict, ask yourself: “What business rule was chosen here?” Look for the answer in the override body—it’s usually written for exactly that reason.
7. Common mistakes with multiple interface implementation
Mistake #1: expecting Kotlin to “take the first interface in the list.”
Some students, out of habit, think that class X : A, B means “if there’s a conflict, A wins because it’s earlier.” Kotlin doesn’t do that. It intentionally forces you to make a decision via override; otherwise the code would behave too magically, and debugging would turn into archaeology.
Mistake #2: writing super.ping() during a conflict and being surprised that it doesn’t work.
When there are multiple implementations, super without qualification doesn’t give an unambiguous answer, and the compiler won’t let you “guess.” In such cases you need exactly super<A>.ping() or super<B>.ping(). This syntax isn’t decoration—it’s a way to explicitly specify the source of the implementation.
Mistake #3: “gluing” two implementations together without meaning and getting a weird result.
Technically, you can do super<A>.foo(); super<B>.foo() in almost any case, but logically it’s sometimes absurd: for example, two implementations may represent different output formats or different policies. If you combine behaviors, make it an intentional rule that can be read from the code (for example, via intermediate val variables and a clear result string).
Mistake #4: missing the conflict because “there seems to be only one implementation.”
The “default + abstract” case is often perceived as “well, there is an implementation.” But Kotlin requires override because the contracts overlap. If you don’t understand why the compiler forces you to override the method, check: perhaps the second interface declares the same method without a body. It’s not a bug—it’s a readability safeguard.
Mistake #5: creating interfaces that are too broad and constantly conflict by name.
If you regularly hit conflicts on methods like print(), render(), format(), it may be a signal that the interfaces are too general and overlap in meaning. Sometimes it’s enough to rename the method to something more specific (renderCsvLine(), renderHumanLine()) so the code reads better and there are fewer conflicts.
Mistake #6: trying to hide the conflict with a “universal hack” instead of solving it.
Sometimes you just want “it to compile,” so you write an override that returns something random, ignoring both default implementations. The contract is formally satisfied, but the behavior becomes surprising. If interfaces conflict, it almost always means you need to formulate a rule: “we choose A,” “we choose B,” or “we combine them like this.”
GO TO FULL VERSION