Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Does introducing a default method to an interface really preserve back-compatibility?

Tags:

java-8

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?

like image 543
James_D Avatar asked Mar 24 '14 19:03

James_D


People also ask

Why do we introduce default method in interface?

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.

Which mechanism is used for preserving backward compatibility when extending the functionality of the interface?

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.

Do we need to implement default method of interface?

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.

Are interface methods final by default?

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.


2 Answers

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:

  • Compatibility Guide for JDK 8.
like image 60
Rohit Jain Avatar answered Oct 14 '22 12:10

Rohit Jain


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     } } 
like image 32
charlie Avatar answered Oct 14 '22 12:10

charlie