JAVA / ABSTRACT CLASSES AND INTERFACES
Multiple interfaces and resolving method conflicts
Combine several interfaces on one class and resolve clashing inherited methods with an override plus Interface.super calls.
What you will learn
- Override the clashing method in the class to settle two unrelated defaults
- Reuse a chosen parent body with Interface.super.method() inside the override
- Apply precedence: superclass concrete methods win, subinterfaces beat superinterfaces
- Recognize that an abstract method plus an unrelated default still needs an override
Understanding Multiple interfaces and resolving method conflicts
A class may implement any number of interfaces, and most of the time nothing collides: interfaces carry no instance fields and no constructors, so there is no duplicated state to reconcile. Two interfaces that both declare String id(); are satisfied by a single method in the class, because abstract signatures merge rather than compete. The trouble starts when the inherited members have bodies: if Console and FileSink each supply a default render(String), the class inherits two runnable implementations of one signature and the compiler has no basis for choosing between them.
Java settles such clashes with two precedence rules and one refusal. A concrete method inherited from a superclass always beats an interface default, and a declaration in a subinterface beats the declaration it overrides in a superinterface, so whenever one candidate is strictly more specific the compiler picks it without saying anything. If neither rule applies, compilation fails at the class declaration with a complaint about inheriting unrelated defaults, and the order of names in the implements clause is never consulted.
Fixing the error means declaring the method in the class, which stops both defaults from being inherited and puts the decision in your hands. Inside that override, Console.super.render(msg) reaches one specific inherited body, so you can forward to one interface, combine both, or ignore them entirely. The qualifier must name a direct superinterface that still owns the method, so you cannot reach an interface that is only visible through another interface. Because the choice lives in the class, a call through a Console reference and a call through a FileSink reference both land on the same method.
placeholder
interface Console {
default String render(String msg) {
return "[console] " + msg;
}
}
interface FileSink {
default String render(String msg) {
return "[file] " + msg;
}
}
class DualSink implements Console, FileSink {
@Override
public String render(String msg) {
return Console.super.render(msg) + " | " + FileSink.super.render(msg);
}
}
public class Main {
public static void main(String[] args) {
DualSink sink = new DualSink();
System.out.println(sink.render("disk full"));
Console asConsole = sink;
FileSink asFile = sink;
System.out.println(asConsole.render("retry 1"));
System.out.println(asFile.render("retry 2"));
}
}When a class inherits two bodies for one signature, Java chooses for you only if one candidate is more specific; otherwise you must override and name your pick with Interface.super.
Worked examples
The more specific interface wins
Shows that no override is needed when one of the implemented interfaces extends the other.
interface Shape {
default String name() {
return "shape";
}
}
interface Polygon extends Shape {
@Override
default String name() {
return "polygon";
}
}
class Square implements Shape, Polygon {
}
public class Main {
public static void main(String[] args) {
System.out.println(new Square().name());
Shape s = new Square();
System.out.println(s.name());
}
}Example explained
Line 1Polygon extends Shape and redeclares name(), so Polygon.name() overrides Shape.name().
Line 2Square therefore inherits one winner, and listing Shape in the implements clause adds nothing.
Line 3No override is required in Square and no ambiguity is reported, because the candidates are related.
Line 4The call through a Shape reference still runs Polygon's body, since dispatch follows the object.
A superclass method outranks a default
Demonstrates that an inherited concrete class method takes precedence over an interface default with the same signature.
class Base {
public String id() {
return "base";
}
}
interface Tagged {
default String id() {
return "tagged";
}
}
class Item extends Base implements Tagged {
}
public class Main {
public static void main(String[] args) {
Item item = new Item();
System.out.println(item.id());
Tagged t = item;
System.out.println(t.id());
}
}Example explained
Line 1Base.id() is public with a matching signature, so it already satisfies the Tagged contract.
Line 2Item declares no members at all, yet it compiles: the inherited class method outranks the default.
Line 3Both lines print base, so the default body is simply out of the picture for this class.
Abstract in one interface, default in another
Shows that an unrelated interface's default does not implement another interface's abstract method.
interface Encoder {
String encode(String s);
}
interface Uppercaser {
default String encode(String s) {
return s.toUpperCase();
}
}
class Codec implements Encoder, Uppercaser {
@Override
public String encode(String s) {
return Uppercaser.super.encode(s) + "!";
}
}
public class Main {
public static void main(String[] args) {
Encoder e = new Codec();
System.out.println(e.encode("ready"));
}
}Example explained
Line 1Encoder.encode is abstract and Uppercaser.encode has a body, but neither overrides the other because the interfaces are unrelated.
Line 2Remove the method from Codec and it stops compiling: the class inherits an abstract and a default declaration for the same signature.
Line 3Uppercaser.super.encode(s) runs the inherited default body from inside the class's own method.
Line 4The call through an Encoder reference reaches Codec.encode, so the exclamation mark is appended.
Important notes
Constants collide too: if two implemented interfaces both declare LIMIT, an unqualified LIMIT inside the class is ambiguous and must be written Console.LIMIT or FileSink.LIMIT.
If the clashing methods differ only in return type, such as int size() and String size(), no override can rescue the class, because one method cannot have two return types; one of the interfaces has to change.
Common mistakes
Assuming implements Console, FileSink means Console wins; the order carries no meaning and the class still fails to compile until you override.
Writing super.render(msg) instead of Console.super.render(msg) inside the override; plain super refers to the superclass, here Object, so the call does not compile.
Marking the class abstract to postpone the decision; an abstract class inherits the two conflicting defaults exactly the same way and the error is unchanged.
Try it yourself
Change, predict, then run
Write Metric and Label, each with a default String tag() returning a different word, then a class Gauge implements Metric, Label whose tag() returns both parent results joined by a slash. Print it through a Metric variable, then delete the override and read the compiler error.
Open the Java workspaceCheck your understanding
A class implements two unrelated interfaces A and B that both declare a default void sync(). Which change makes the class compile without editing either interface?
- Reorder the implements clause so the preferred interface is listed first
- Declare the class abstract so subclasses deal with the clash
- Declare sync() in the class itself, optionally delegating with A.super.sync()
- Cast the receiver to the preferred interface at every call site
Show answer
The ambiguity belongs to the class declaration, not to any particular call, so it can only be settled by declaring sync() in the class; once that declaration exists neither default is inherited, and A.super.sync() can still reuse one body. Marking the class abstract feels like deferring the problem, but an abstract class inherits both defaults just as a concrete one does and the same error is reported; casting only changes the static type of an expression and cannot affect which method the class inherits.