Default Methods in Interface: Multiple Inheritance and the Diamond Problem in Java
-
Last Updated: October 22, 2026
-
By: javahandson
-
Series

Learn default methods in interface in Java 8: syntax, overriding, static methods, and the diamond problem with its three resolution rules, in plain language.
For a long time, a Java interface could only hold method signatures. It told you what to do, never how to do it. That changed in Java 8. With default methods in interface, you can now write a real method body right inside the interface. So the interface can hand a class ready-made behaviour, not just a promise.
This one feature looks small. However, it quietly solved a big problem and also opened a new one. In this guide, we will slow down and understand both sides.
We will start with the reason this feature exists. After that, we will write our first default method and see how the JDK itself uses them. Finally, we will face the famous diamond problem and learn the exact rules Java uses to resolve it.
By the end, you will be able to:
Let us begin.
To really get this feature, you must first feel the pain it removes. So let me take you back to the problem.
Before Java 8, every method in an interface was abstract. It had no body. Any class that implemented the interface had to provide the body for each method. This rule was strict, and it kept things clean.
Here is a simple interface from the old days:
public interface Vehicle {
void start();
void stop();
}Every class that implements Vehicle must write both start() and stop(). That sounds fair. But now think about what happens when the interface needs to grow.
Imagine Vehicle sits behind a hundred classes across many teams. One day, you decide to add a new method called honk(). You update the interface like this:
public interface Vehicle {
void start();
void stop();
void honk(); // new method
}Now every single one of those hundred classes stops compiling. Each class is missing honk(). So one small change breaks the whole codebase. This is not a bug. This is just how abstract methods work.
The Java team hit this exact wall. They wanted to add new methods to core interfaces like List and Collection. The new Stream API needed a stream() method on every collection. However, millions of programs already implemented those interfaces. One new abstract method would break all of them at once.
Before default methods existed, library authors used a clever but clumsy trick. They put the shared logic in a separate helper class full of static methods. The Collections class is the classic example. It holds static helpers like Collections.sort() and Collections.reverse().
So the behaviour lived outside the interface, in a utility class. This worked, yet it felt awkward. You had to remember two names: the interface and its helper. Also, the helper methods could not be overridden per class. Clearly, there was room for something better.
So the Java team needed a way to add a method that does not force every class to change. The answer was the default method. A default method comes with a ready body. Old classes simply inherit that body and keep working. New classes can override it when they want something different.
That is how stream() was added to Collection without breaking old code. In short, default methods let an interface evolve in a safe way. This idea is also called interface evolution, and it is the real reason this feature was born.
| 💡 Interview Insight Interviewers love to ask why default methods were added to Java 8. The short answer is backward compatibility. They allowed new methods, such as stream() on the Collection interface, to be added without breaking the millions of classes that already implemented it. Say this clearly and you score the point. |
A default method is a method inside an interface that carries its own body. You mark it with the default keyword. Because it already has a body, a class is free to use it as-is.
The shape is easy to remember. You write default, then the return type, then the method with a body:
public interface Vehicle {
void start(); // abstract, no body
void stop(); // abstract, no body
// default method with a ready body
default void honk() {
System.out.println("Beep beep!");
}
}Notice the difference. Both start() and stop() have no body, so they stay abstract. On the other hand, honk() has a body, so it is a default method. The interface now mixes both kinds, and that is perfectly legal.
Let us implement the interface in a class. The class only needs to handle the abstract methods. It gets honk() for free:
public class Car implements Vehicle {
public void start() {
System.out.println("Car starting...");
}
public void stop() {
System.out.println("Car stopping...");
}
// no honk() here — it comes from the interface
}Now you can call honk() on a Car object even though Car never wrote it:
Car car = new Car(); car.start(); // Car starting... car.honk(); // Beep beep! (from the interface)
That is the whole magic. The behaviour lives in the interface, yet the object can run it. As a result, you can share common behaviour across many classes without a base class.
Default methods follow a few simple rules. Keep these in mind and you will avoid most mistakes:
That last point trips up many beginners. An interface holds no instance variables. So a default method can only work with its parameters, other interface methods, and constants. It cannot store data between calls.
Here is something nice. You have probably used default methods for years without noticing. The JDK is full of them. Seeing real ones makes the idea click faster.
Every collection has a forEach() method. You call it all the time. It is a default method added to the Iterable interface in Java 8:
List<String> names = List.of("Asha", "Ravi", "Meera");
names.forEach(name -> System.out.println(name));Before Java 8, this method did not exist. It was added as a default method, so every existing List, Set, and Queue got it at once. No class had to change.
The Map interface gained several default methods too. A handy one is getOrDefault(). It returns a fallback value when a key is missing:
Map<String, Integer> scores = new HashMap<>();
scores.put("Asha", 90);
int asha = scores.getOrDefault("Asha", 0); // 90
int ravi = scores.getOrDefault("Ravi", 0); // 0, key absentThis saves you from writing a null check every time. Again, it slipped into the Map interface as a default method, so HashMap and TreeMap both got it for free.
Another favourite is removeIf(). It removes every element that matches a condition. You pass a simple test, and the method does the loop for you:
List<Integer> numbers = new ArrayList<>(List.of(1, 2, 3, 4, 5, 6)); numbers.removeIf(n -> n % 2 == 0); // drop the even numbers System.out.println(numbers); // [1, 3, 5]
So removeIf() replaced a clunky iterator loop with one clean line. It lives on the Collection interface as a default method. As a result, every list and set gained it in Java 8 with no code change on their side.
The Comparator interface shows the pattern at its best. It has one abstract method, compare(). Around it sit many default methods, such as reversed() and thenComparing(). These let you chain comparators in a clean way:
Comparator<String> byLength = Comparator.comparingInt(String::length);
// reversed() and thenComparing() are default methods
List<String> list = new ArrayList<>(List.of("bb", "a", "ccc", "dd"));
list.sort(byLength.reversed().thenComparing(Comparator.naturalOrder()));So reversed() does not need its own class. It is a default method that wraps the current comparator. This is default methods at their most elegant. One small abstract method, many rich helpers built on top.
Look back at those examples. Each one follows the same idea. The interface keeps its core abstract methods. Then it bolts on convenience methods as defaults. So every class that implements the interface gets the extras for free.
This is the real spirit of the feature. A default method is not meant to carry your main logic. It is meant to offer a small, shared helper that most classes will want anyway. Keep this picture in your head and your own interfaces will turn out clean.
A default method is only a default. It is a suggestion, not a law. So any class can replace it with its own version. You have three choices, and it is worth knowing each one.
The simplest choice is to do nothing. The class inherits the default body and uses it. We already saw this with Car and honk(). This works well when the default behaviour fits every class.
Sometimes the default does not suit a class. In that case, you override the method and write fresh logic. A sports car may want a louder horn:
public class SportsCar implements Vehicle {
public void start() {
System.out.println("Sports car roaring to life...");
}
public void stop() {
System.out.println("Sports car stopping...");
}
@Override
public void honk() {
System.out.println("HOOONK! Out of my way!");
}
}Here SportsCar replaces honk() entirely. The interface body is skipped for this class. Also notice the @Override annotation. It is optional, yet it makes your intent clear and catches typos.
There is a third, elegant option. You can override the method but still call the original default body inside it. The syntax uses the interface name followed by super:
public class LuxuryCar implements Vehicle {
public void start() { System.out.println("Starting..."); }
public void stop() { System.out.println("Stopping..."); }
@Override
public void honk() {
Vehicle.super.honk(); // run the default body first
System.out.println("(polite honk)"); // then add extra behaviour
}
}The line Vehicle.super.honk() calls the default method from the interface. After that, the class adds its own touch. This pattern is handy when you want to build on top of the default instead of throwing it away. Keep this exact syntax in mind, because it returns in the diamond problem.
Let me show a case from everyday work. Suppose your app has a Notifier interface with one method, send(). Many classes implement it: EmailNotifier, SmsNotifier, and more. Now the team wants a new method, sendUrgent(), but you cannot break those classes.
public interface Notifier {
void send(String message);
// new method, added safely as a default
default void sendUrgent(String message) {
send("[URGENT] " + message); // reuse the existing method
}
}Notice the trick. The default sendUrgent() simply calls the existing send() with a prefix. So every old class gets urgent notifications at once, with zero changes. Later, a class like SmsNotifier can override sendUrgent() to flash a priority flag. This is interface evolution in your own code, not just in the JDK.
This pattern is worth remembering for real projects. You add value to an interface without a painful migration. Each team updates its own classes later, at its own pace. Meanwhile, nothing breaks on day one. That calm, safe rollout is exactly what default methods were built for.
Here is a question that confuses many people. A functional interface must have exactly one abstract method. So can it also have default methods? Yes, it can. Default methods do not count against that rule.
The rule only counts abstract methods. So a functional interface can carry any number of default methods. That is why Comparator is still a functional interface even with its many default methods. It has just one abstract method, compare().
@FunctionalInterface
interface Calculator {
int apply(int a, int b); // the single abstract method
// default helpers are allowed
default int applyTwice(int a, int b) {
return apply(apply(a, b), b);
}
}
Calculator add = (a, b) -> a + b; // lambda still works
System.out.println(add.applyTwice(5, 3)); // 11So you still write a lambda for the one abstract method. Meanwhile, the default method gives you a bonus feature for free. This combination is powerful and very common in modern Java.
The JDK shows this everywhere. Predicate is a functional interface with one abstract test() method. Yet it also carries default methods like and(), or(), and negate(). These let you combine simple checks into bigger ones:
Predicate<Integer> positive = n -> n > 0; Predicate<Integer> even = n -> n % 2 == 0; // and() and negate() are default methods on Predicate Predicate<Integer> positiveAndEven = positive.and(even); System.out.println(positiveAndEven.test(4)); // true
So the lambda defines the basic test. Then the default methods build richer logic on top. This is a clean example of default methods doing real work in the standard library.
| 💡 Interview Insight Expect this one: can a functional interface have default methods? Yes. The @FunctionalInterface rule limits only abstract methods to one. Default methods are allowed in any number. Comparator and Predicate are textbook examples to quote. |
Now we reach the tricky part. Default methods gave interfaces real bodies. But a class in Java can implement many interfaces at once. So what happens when two interfaces offer the same default method? Which body wins?
This clash is called the diamond problem. The name comes from the shape of the inheritance diagram, which looks like a diamond.

Let us build the problem step by step. Suppose we have two interfaces. Both declare a default method with the same name, greet():
interface Father {
default String greet() {
return "Hello from Father";
}
}
interface Mother {
default String greet() {
return "Hello from Mother";
}
}So far, so good. Each interface is fine on its own. The trouble starts when one class implements both of them:
// This will NOT compile
class Child implements Father, Mother {
// which greet() does Child inherit?
}The compiler now sees two equally valid greet() methods. It cannot choose one for you. So it refuses to compile and shows an error like this:
error: class Child inherits unrelated defaults for greet() from types Father and Mother
This is the diamond problem in plain form. Two parents, one method name, no clear winner. In some older languages, this ambiguity caused silent and confusing bugs. Java chose a safer path. It stops you at compile time and asks you to decide.
The Father and Mother names are easy to follow. But the clash shows up in real code too. Imagine two small interfaces. One is Logger, the other is Auditor. Both decide to offer a default record() method:
interface Logger {
default void record(String msg) {
System.out.println("LOG: " + msg);
}
}
interface Auditor {
default void record(String msg) {
System.out.println("AUDIT: " + msg);
}
}
// a service wants both abilities — but record() clashes
class OrderService implements Logger, Auditor {
// will not compile until record() is resolved
}Here nobody did anything wrong. Two useful interfaces just happened to share a method name. Yet the moment OrderService uses both, the build breaks. So the diamond problem is not only a textbook puzzle. It can appear whenever you mix interfaces from different sources.
You might wonder why Java does not just pick one automatically. The reason is safety. If the compiler guessed, you could get the wrong behaviour without knowing. A failing build is loud and clear. A silent wrong answer is far more dangerous. So Java makes the ambiguity your problem to solve, on purpose.
| 💡 Interview Insight Interviewers often ask how Java handles the diamond problem of multiple inheritance. The key point: Java does not resolve the clash silently. When two interfaces give the same default method, the class must override it, or the code will not compile. Mention that the resolution happens at compile time, not at runtime. |
Java follows three clear rules to decide which method wins. Learn them in order, because the compiler checks them in order. Once you know them, the diamond problem stops being scary.
The first rule is simple. A method from a class or a superclass always beats a default method from an interface. In other words, concrete code wins over default code.
interface Greeter {
default String greet() { return "Hi from interface"; }
}
class BaseGreeter {
public String greet() { return "Hi from base class"; }
}
class MyClass extends BaseGreeter implements Greeter {
// no greet() here
}
// result: "Hi from base class" — the class winsEven though Greeter offers a default greet(), the body from BaseGreeter runs. The class method simply has higher priority. People often state this rule as “the class always wins”.
The second rule applies when one interface extends another. Here the child interface is more specific. So its default method beats the parent’s default method.
interface Animal {
default String sound() { return "Some sound"; }
}
interface Dog extends Animal {
@Override
default String sound() { return "Woof"; }
}
class Puppy implements Dog, Animal {
// no sound() here
}
// result: "Woof" — Dog is more specific than AnimalDog sits lower in the hierarchy than Animal. Therefore its version of sound() is chosen. The compiler always picks the most specific interface in the chain. You do not need to add anything extra.
The first two rules work when there is a clear winner. But our Father and Mother example had no winner. The two interfaces were unrelated, and neither was more specific. In that case, the third rule kicks in. You must override the method yourself and pick the body you want.
You pick a parent’s version using the InterfaceName.super.method() syntax we saw earlier:
class Child implements Father, Mother {
@Override
public String greet() {
// explicitly choose Father's version
return Father.super.greet();
}
}
// result: "Hello from Father"Now the code compiles happily. The Child class clearly states its choice. You could just as easily pick Mother.super.greet() instead. You can even combine both versions:
@Override
public String greet() {
return Father.super.greet() + " and " + Mother.super.greet();
}
// result: "Hello from Father and Hello from Mother"So the third rule hands full control back to you. Instead of guessing, Java makes you write down exactly what you mean. This keeps the behaviour clear for anyone who reads the class later.
| 💡 Interview Insight A favourite follow-up: how do you call a specific interface’s default method when two of them clash? The answer is the InterfaceName.super.method() syntax, for example Father.super.greet(). Many candidates know the diamond problem but forget this exact syntax. Remember it and you stand out. |
People often mix up default methods and plain abstract methods inside an interface. So here is a side-by-side view to keep them straight:
| Aspect | Abstract Method | Default Method |
|---|---|---|
| Has a body? | No | Yes |
| Keyword used | none | default |
| Must a class implement it? | Yes | No, it is optional |
| Can a class override it? | Must | Yes, if it wants to |
| Holds instance state? | No | No |
| Part of diamond problem? | No | Yes |
Read the last row carefully. Only default methods take part in the diamond problem. Abstract methods have no body to clash, so they never collide. This is why the diamond problem is a default-method story.
The third row matters a lot in daily work. An abstract method forces every class to write it. A default method does not. So when you add a default method to a live interface, old classes keep compiling. That single difference is what makes safe interface evolution possible.
Default methods are powerful, yet they are easy to misuse. Let me share a few lessons that save real debugging time.
A default method is best for safe interface evolution or for a small, shared convenience. It is not a replacement for a proper base class. If you find yourself writing lots of logic in default methods, stop. An abstract class is often the cleaner home for shared behaviour with state.
Remember, an interface has no fields. So a default method cannot hold state. Keep it short and stateless. A good default method usually calls other interface methods and adds a thin layer of logic on top.
Avoid these five traps and your interfaces will stay clean. Most diamond-problem confusion comes from the very first trap on that list.
This choice comes up a lot, so let me make it simple. Both can hold shared behaviour. However, they are not the same tool. Each one fits a different job.
Reach for a default method when you want to add behaviour to an interface without breaking the classes that already use it. It shines for small, stateless helpers. Reach for an abstract class when you need real fields, a constructor, or private helper methods. An abstract class can hold state, while an interface cannot.
A few quick pointers help here:
In practice, many designs use both together. An interface defines the contract with a few defaults. An abstract class then provides a base with state. So you do not always have to pick just one.
| 💡 Interview Insight Interviewers sometimes ask when you would choose a default method over an abstract class. A solid answer: use a default method to add behaviour to an interface without breaking existing implementors, especially when there is no shared state. Choose an abstract class when you need fields, a constructor, or a strong is-a relationship. |
A: A default method is a method inside an interface that has its own body, marked with the default keyword. It was added in Java 8 so an interface can provide ready-made behaviour that implementing classes inherit automatically, instead of only declaring abstract methods.
A: They were added for backward compatibility. The Java team needed to add new methods like stream() to the Collection interface without breaking the millions of classes that already implemented it. A default method ships with a body, so old classes keep working and only new classes override it when needed.
A: The diamond problem happens when a class implements two interfaces that both provide a default method with the same signature. Java cannot decide which body to inherit, so the code fails to compile with an “inherits unrelated defaults” error.
A: Java uses three rules. First, a method in a class or superclass beats any interface default. Second, a more specific sub-interface beats its parent interface. Third, when neither applies, you override the method yourself and pick a version using the InterfaceName.super.method() syntax, for example Father.super.greet().
A: Yes. Since Java 8, an interface can have static methods. They belong to the interface itself and are called using the interface name, such as Vehicle.createBasicCar(). Static interface methods are not inherited by implementing classes, so they never take part in the diamond problem.
A: Yes. The @FunctionalInterface rule limits only the number of abstract methods to one. Default and static methods are allowed in any number. Comparator is the classic example — it has one abstract method, compare(), alongside many default methods like reversed() and thenComparing().
Default methods in interface changed how Java interfaces work. They let an interface carry real behaviour, not just method names. More importantly, they let core interfaces grow without breaking old code. That alone makes them one of the most practical features of Java 8.
Here is a quick recap of the whole journey:
Start with a small interface and one default method. Then try a deliberate clash between two interfaces and fix it with InterfaceName.super. Once you do this by hand, the rules will stick for good.