I think I'm slightly confused by the introduction of default methods to interfaces in Java. As I understand it, the idea is that default methods can be introduced to existing interfaces without breaking existing code.
If I implement an interface with a non-abstract class, I (of course) have to define implementations of all the abstract methods in the interface. If the interface defines a default method, I inherit the implementation of that method.
If I implement two interfaces, I obviously have to implement the union of the abstract methods defined in both interfaces. I inherit the implementation of all the default methods; however if there happens to be a collision between default methods in the two interfaces, I must override that method in my implementing class.
This sounds fine, but what about the following scenario?
Suppose there's an interface:
package com.example ; /** * Version 1.0 */ public interface A { public void foo() ; /** * The answer to life, the universe, and everything. */ public default int getAnswer() { return 42 ;} }
and a second interface
package com.acme ; /** * Version 1.0 */ public interface B { public void bar() ; }
So I can write the following:
package com.mycompany ; public class C implements com.example.A, com.acme.B { @Override public void foo() { System.out.println("foo"); } @Override public void bar() { System.out.println("bar"); } public static void main(String[] args) { System.out.println(new C().getAnswer()); } }
So that should be fine, and indeed
java com.mycompany.C
displays the result 42.
But now suppose acme.com makes the following change to B:
package com.acme ; /** * Version 1.1 */ public interface B { public void bar() ; /** * The answer to life, the universe, and everything * @since 1.1 */ public default int getAnswer() { return 6*9; } }
As I understand it, introducing this method is supposed to be safe. But if I now run the existing com.mycompany.C against the new version, I get a runtime error:
Exception in thread "main" java.lang.IncompatibleClassChangeError: Conflicting default methods: com/example/A.getAnswer com/acme/B.getAnswer at com.mycompany.C.getAnswer(C.java) at com.mycompany.C.main(C.java:12)
That's not entirely surprising, but doesn't it mean that introducing default methods to existing interfaces always runs the risk of breaking existing code? What am I missing?
The default methods were introduced to provide backward compatibility so that existing interfaces can use the lambda expressions without implementing the methods in the implementation class. Default methods are also known as defender methods or virtual extension methods.
Along with lambda expressions, a new language construct is introduced: default methods in interfaces. The intent of this feature is to allow interfaces to be extended over time preserving backward compatibility.
Why Interfaces Need Default Methods. Like regular interface methods, default methods are implicitly public; there's no need to specify the public modifier. Unlike regular interface methods, we declare them with the default keyword at the beginning of the method signature, and they provide an implementation.
It is used to provide total abstraction. That means all the methods in an interface are declared with an empty body and are public and all fields are public, static, and final by default. A class that implements an interface must implement all the methods declared in the interface.
Although adding a default method with the same name in the two interfaces would make the code fail to compile, but once you resolve the compilation error, the binaries obtained after compiling both the interfaces, and the class implementing the interfaces, would be backward compatible.
So, the compatibility is really about binary compatibility. This is being explained in JLS §13.5.6 - Interface Method Declarations:
Adding a default method, or changing a method from abstract to default, does not break compatibility with pre-existing binaries, but may cause an
IncompatibleClassChangeError
if a pre-existing binary attempts to invoke the method. This error occurs if the qualifying type, T, is a subtype of two interfaces, I and J, where both I and J declare a default method with the same signature and result, and neither I nor J is a subinterface of the other.In other words, adding a default method is a binary-compatible change because it does not introduce errors at link time, even if it introduces errors at compile time or invocation time. In practice, the risk of accidental clashes occurring by introducing a default method are similar to those associated with adding a new method to a non-final class. In the event of a clash, adding a method to a class is unlikely to trigger a LinkageError, but an accidental override of the method in a child can lead to unpredictable method behavior. Both changes can cause errors at compile time.
The reason you got the IncompatibleClassChangeError
is probably because, you didn't recompile your C
class, after adding the default method in B
interface.
Also see:
And even if you update your implementation by explicitly choosing which interface to delegate the conflicting default method call to, a subtle change in one of those interfaces can still render your code not compilable.
E.g. you can fix a class T
like this:
interface I { default void m() {} } interface J { default void m() {} } class T implements I, J { @Override public void m() { // forced override I.super.m(); // OK } }
Everything will be OK, until a change like this:
interface J extends I { @Override default void m() {} }
If only the interface J
is recompiled, the method T::m
will still delegate to I::m
. But compilation of T
itself will no longer succeed — it will fail with an error: bad type qualifier I in default super call
:
class T implements I, J { // redundant I, but not an error @Override public void m() { // override not necessary, T::m resolves to J::m I.super.m(); // ERROR } }
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With