Introduction
The Default Methods in Java allows us to introduce a new method to an existing interface without breaking the existing implementation classes of it. In Java, we can extend only one class but can implement many interfaces. But with default methods in the interfaces, there is a chance that a class can inherit more than one method with the same signature. In that case, we need resolution rules for the default methods to pick one of the implementations. We will now look at the Java 8 default methods resolution rules.
Default methods conflict
In normal circumstances (without default methods), when a class implements multiple interfaces, even if more than one interface has a method with the same signature, there will not be a problem of conflict. This is because the implementing class will have to override that method and provide an implementation.
public interface A {
void print();
}
public interface B {
void print();
}
public class C implements A, B {
// Has to provide an implementation of the print method
}
But what if both interfaces A and B provide a default method? i.e., a default implementation for the print method.
Now, here arises a conflict or an ambiguity in class C as there must be rules to define which print method is inherited (spoiler: none of them!!).
Rules for Default Method Conflict Resolution
When a class inherits a method with the same signature from more than one interface, we have the following three rules:
- Classes will always win. If a class extends a parent class and implements one or more interfaces, the default method in the class or a superclass will take priority.
- Otherwise the subinterfaces will take the next level of precedence. The default method in the most specific default providing interface will be the chosen one. (A subinterface is the one that extends an interface).
- If the above rules are not applicable and a choice cannot be made, then the class that inherits from multiple interfaces has to override the method and provide an implementation. But it does not always have to provide the full implementation as there is a way for the class to choose one of the inherited default methods.
Most specific default providing interface (rule 2)
Let us say we have an interface MyInterfaceA
and MyInterfaceB
and where MyInterfaceB
extends MyInterfaceA
. Both have a default method. We create a class that extends both these interfaces.
public interface MyInterfaceA {
default void print() {
System.out.println("Print in MyInterfaceA");
}
}
public interface MyInterfaceB extends MyInterfaceA {
default void print() {
System.out.println("Print in MyInterfaceB");
}
}
public class MyClass implements MyInterfaceA, MyInterfaceB {
}
What would the below print? Which print() method would it invoke?
MyClass myClass = new MyClass();
myClass.print();
The default method resolution rule 2 will be applicable here (the most specific implementation rule). Since, MyInterfaceB
is the most specific default providing interface, it will be selected and thus above will print Print in MyInterfaceB
.
Conflict resolution with both a super class and interfaces
What if we have the following scenario?
public interface MyInterfaceA {
default void print() {
System.out.println("Print in MyInterfaceA");
}
}
public interface MyInterfaceB extends MyInterfaceA {
default void print() {
System.out.println("Print in MyInterfaceB");
}
}
public class MyClass1 implements MyInterfaceA {
}
public class MyClass2 extends MyClass1 implements MyInterfaceA, MyInterfaceB {
}
MyClass1 myClass1 = new MyClass1();
myClass1.print();
MyClass2 myClass2 = new MyClass2();
myClass2.print();
Calling the print method on an instance of MyClass1 will call the default method it inherits from MyInterfaceA.
What would it print when we call the print method on MyClass2?
Rule 1 says the method from a class will take higher priority over other interfaces. But MyClass1 does not provide an implementation. Thus, the resolution rule 1 cannot be applied.
Rule 2 picks the most specific default providing interface. Since MyInterfaceB is more specific, calling myClass2.print()
prints Print in MyInterfaceB
.
Classes always win (Rule 1)
If MyClass1 overrides the print method like
public class MyClass1 implements MyInterfaceA {
@Override
public void print() {
System.out.println("Print from MyClass1");
}
}
Now, calling myClass2.print()
will result in calling the print method in MyClass1. This is because of the resolution rule 1. To MyClass2, there are two print implementations available viz., one from MyClass1 and one from the most specific subinterface MyInterfaceB. Since rule 1 is of higher priority than rule 2, it would pick a class before a subinterface.
When the method in the super class is abstract
Let us tweak the above example by making MyClass1 abstract and declare a print method in it as an abstract method.
public abstract class MyClass1 implements MyInterfaceA {
public abstract void print();
}
MyClass2 no longer complies!
It is forced to implement the print method even though a default implementation is available from MyInterfaceB.
public class MyClass2 extends MyClass1 implements MyInterfaceA, MyInterfaceB {
@Override
public void print() { //Have to override
System.out.println("Print in MyClass2");
}
}
Choosing one of the inherited method’s implementation
As I said earlier, we don’t always have to implement the full method. It is possible to call one of the available default methods from the inheritance hierarchy.
Example: In the above example, we were forced to override the method. But if we want to have the same print method logic from MyInterfaceB, we don’t have to copy it over into MyClass2.
Java 8 introduced a new syntax X.super.method(..)
. Here, X is some superclass or interface and method is the method we want to call. Using this, from MyClass2’s print method we can call the MyInterfaceB’s print method as:
public class MyClass2 extends MyClass1 implements MyInterfaceA, MyInterfaceB {
@Override
public void print() { //Have to override
MyInterfaceB.super.print(); //Call the MyInterfaceB's default method
}
}
Conflict resolution (Rule 3)
Let us look at an example for conflict where we would be forced to override a method because none of the default methods could be chosen by rules 1 or 2.
public interface MyInterfaceA {
default void print() {
System.out.println("Print in MyInterfaceA");
}
}
public interface MyInterfaceB {
default void print() {
System.out.println("Print in MyInterfaceB");
}
}
public class MyClass implements MyInterfaceA, MyInterfaceB {
@Override
public void print() {
System.out.println("Print in MyClass");
//and/or
MyInterfaceA.super.print();
}
}
The above example is like the first example, but MyInterfaceB does not extend MyInterfaceA. Because of this, there will be no subinterface and hence rule 2 cannot be applied. Thus, MyClass2 will be forced to override the print method. As we learnt earlier, either we can override by providing an implementation or we can call the print method of one of the interfaces.
Superclass with overridden method takes priority
Let us have MyInterfaceA and MyInterfaceB as in the previous example. In addition to this, we have MyClass1 as follows.
public class MyClass1 implements MyInterfaceA {
@Override
public void print() {
System.out.println("Print from MyClass1");
}
}
Change MyClass2 to extend MyClass1 in addition to implementing MyInterfaceA and MyInterfaceB.
public class MyClass2 extends MyClass1 implements MyInterfaceA, MyInterfaceB {
}
Now, we don’t have to override the print method in MyClass2. Why?
Because of the default method resolution rule 1, the implementation from a superclass will be of higher priority. Thus it will be chosen and calling myClass2.print()
, will print Print from MyClass1
.
The Diamond problem with the default methods
The diamond problem is the ambiguity that arises because of multiple inheritance. Let us say we have the following hierarchy.
public interface A {
default void print() {
System.out.println("Print in A");
}
}
public interface B extends A {
}
public interface C extends A {
}
public class D implements B, C {
}
new D().print();
Calling print() on an instance of D has only one method to pick, the default implementation from interface A.
One of the interfaces defines a default method
If interface B (or C) also defines a default method for print, resolution rule 2 will be used as it will be the most specific default providing interface.
public interface B extends A {
@Override
default void print() {
System.out.println("Print from B");
}
}
Calling new D().print()
will print Print from B
.
The multiple inheritance ambiguity
What happens when both B and C define a default method in each of them.
public interface B extends A {
@Override
default void print() {
System.out.println("Print from C");
}
}
public interface C extends A {
@Override
default void print() {
System.out.println("Print from C");
}
}
Class D will not compile and we have to override the print method by providing an implementation or call one of the interfaces like A.super.print()
or B.super.print()
.
Did you notice that with default methods we indirectly have multiple inheritance problem? Java does not allow multiple inheritance, and hence the diamond problem was not there in Java. With default methods, we are back to the multiple inheritance problem - class D inherits a method with a same signature from both B and C.
What if interface C adds an abstract print method (not the default implementation)?
public interface A {
default void print() {
System.out.println("Print in A");
}
}
public interface B extends A {
}
public interface C extends A {
void print();
}
public class D implements B, C {
@Override
public void print() { //Forced to override
System.out.println("Print in D");
}
}
Since C has a normal (abstract) interface method called print(), class D has to provide an implementation for the print method. This is true even if interface B provides a default print() method.
Conclusion
This post covered the Java 8 default method resolution rules. First, we looked at the problem of a conflict or ambiguity when inheriting a method with the same signature from more than one interface. Then we looked at examples to cover all the three default methods resolution rules. Finally, we learnt that the diamond problem (multiple inheritance ambiguity) is back in Java because of the default methods. To solve the conflict (when neither of the first two rules are applicable), we have to override the conflicting method.