Java 8 在接口中引入默认方法真的能保持向后兼容性吗?

Java 8 在接口中引入默认方法真的能保持向后兼容性吗?,java-8,Java 8,我想我对Java中接口默认方法的引入有点困惑。据我所知,其想法是可以在不破坏现有代码的情况下将默认方法引入现有接口 如果我用非抽象类实现接口,我(当然)必须定义接口中所有抽象方法的实现。如果接口定义了默认方法,我将继承该方法的实现 如果我实现两个接口,我显然必须实现两个接口中定义的抽象方法的联合。我继承了所有默认方法的实现;但是,如果两个接口中的默认方法之间发生冲突,我必须在实现类中重写该方法 这听起来不错,但是下面的场景呢 假设有一个接口: package com.example ; /**

我想我对Java中接口默认方法的引入有点困惑。据我所知,其想法是可以在不破坏现有代码的情况下将默认方法引入现有接口

如果我用非抽象类实现接口,我(当然)必须定义接口中所有抽象方法的实现。如果接口定义了默认方法,我将继承该方法的实现

如果我实现两个接口,我显然必须实现两个接口中定义的抽象方法的联合。我继承了所有默认方法的实现;但是,如果两个接口中的默认方法之间发生冲突,我必须在实现类中重写该方法

这听起来不错,但是下面的场景呢

假设有一个接口:

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 ;}
}
和第二个接口

package com.acme ;
/** 
* Version 1.0
*/
public interface B {
  public void bar() ;
}
因此,我可以写以下内容:

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());
  }
}
所以这应该是好的,事实上

java com.mycompany.C 
显示结果42

但是现在假设acme.com对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;
  }
}
据我所知,采用这种方法应该是安全的。但是,如果现在针对新版本运行现有的com.mycompany.C,则会出现运行时错误:

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)

这并不完全令人惊讶,但这是否意味着向现有接口引入默认方法总是有破坏现有代码的风险?我缺少什么?

虽然在两个接口中添加同名的默认方法会使代码无法编译,但一旦解决编译错误,编译两个接口后获得的二进制文件以及实现接口的类将向后兼容

所以,兼容性实际上是关于二进制兼容性。下文对此进行了解释:

添加默认方法,或将方法从抽象更改为 默认情况下,不会破坏与现有二进制文件的兼容性,但 如果预先存在二进制文件,则可能导致不兼容ClassChangeError 尝试调用该方法。如果符合条件,则会发生此错误 类型T是两个接口I和J的子类型,其中I和J 声明具有相同签名和结果的默认方法,以及 I和J都不是另一个的子接口

换句话说,添加默认方法是一种二进制兼容的更改 因为它不会在链接时引入错误,即使 在编译时或调用时引入错误。实际上 通过引入默认方法发生意外碰撞的风险 类似于将新方法添加到 非期末班。发生冲突时,将方法添加到类 不太可能触发LinkageError,但会意外覆盖 子对象中的方法可能导致不可预测的方法行为。二者都 更改可能会在编译时导致错误

出现
不兼容ClassChangeError
的原因可能是,在
B
接口中添加默认方法后,没有重新编译
C

另请参见:


即使您通过显式选择将冲突的默认方法调用委托给哪个接口来更新实现,其中一个接口中的细微更改仍然会导致代码无法编译

例如,您可以这样修复类
T

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
    }
}
interface J extends I {
    @Override
    default void m() {}
}
一切都会好的,直到发生这样的变化:

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
    }
}
interface J extends I {
    @Override
    default void m() {}
}
如果仅重新编译接口
J
,则方法
T::m
仍将委托给
I::m
。但是编译
T
本身将不再成功-它将失败,并出现
错误:默认超级调用中的错误类型限定符I

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
    }
}

当然有。它尽可能地将风险降至最低,但是是的。因此,至少在核心库中,向接口添加默认方法是“一次性的”:“我们将在Java 8中进行所有这些库更改,但在Java 9中不添加更多的默认方法”?因为否则,仅仅将VM从Java 8升级到Java 9可能会导致各种现有代码失败:这将是Java理念的巨大变化,不是吗?但这将意味着,如果在接口中添加新的默认方法,比如在Java 9核心库中,那么在我升级时,任何Java应用程序都可能崩溃?“通过引入默认方法发生意外冲突的风险与向非最终类添加新方法的风险类似”,这根本不是真的,是吗?在任何情况下,向类添加方法都不会导致以前可以执行但现在无法执行的现有二进制文件。“出现不兼容ClassChangeError的原因可能是,在B接口中添加默认方法后,您没有重新编译C类。”确定。。。它不会按原样编译。但是假设C类是我从供应商那里买来的应用程序中的一个类,并且在核心库中进行了更改……不,我不是说编译,我是说执行应用程序。(假设您没有访问源代码的权限。)在过去,始终可以保证,如果在版本X上编译和执行类,则在版本X下编译的类文件将在版本Y上执行(如果Y>=X)(只要您遵循某些基本规则,例如不使用com.sun类等)默认方法意味着应用程序供应商不再能够控制其编译后的应用程序是否与未来的JVM版本兼容。“但如果没有编译问题,那么在Java 8上运行的应用程序将在Java 9上运行而不会出现任何问题。”区别在于存在编译问题的情况。例如,变量名为“enum”的代码将在1.4下编译,但不会在1.5或更高版本下编译。霍