向接口引入默认方法是否真的保留了后向兼容性?

Jam*_*s_D 26 java-8

我认为我对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 ;}
}
Run Code Online (Sandbox Code Playgroud)

和第二个界面

package com.acme ;
/** 
* Version 1.0
*/
public interface B {
  public void bar() ;
}
Run Code Online (Sandbox Code Playgroud)

所以我可以写下面的内容:

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());
  }
}
Run Code Online (Sandbox Code Playgroud)

所以这应该没问题,的确如此

java com.mycompany.C 
Run Code Online (Sandbox Code Playgroud)

显示结果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;
  }
}
Run Code Online (Sandbox Code Playgroud)

据我了解,介绍这种方法应该是安全的.但是如果我现在针对新版本运行现有的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)
Run Code Online (Sandbox Code Playgroud)

这并不奇怪,但这并不意味着将默认方法引入现有接口总是存在破坏现有代码的风险吗?我错过了什么?

Roh*_*ain 14

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

因此,兼容性实际上是关于二进制兼容性.这在JLS§13.5.6 - 接口方法声明中进行了解释:

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

换句话说,添加默认方法是二进制兼容的更改,因为它不会在链接时引入错误,即使它在编译时或调用时引入了错误.在实践中,通过引入默认方法发生的意外冲突的风险类似于向非最终类添加新方法的风险.如果发生冲突,向类添加方法不太可能触发LinkageError,但是在子级中意外覆盖该方法可能会导致不可预测的方法行为.这两个更改都可能在编译时导致错误.

你得到的原因IncompatibleClassChangeError可能是因为,CB界面中添加默认方法之后,你没有重新编译你的类.

另见:

  • 不,我不是在谈论编译,我在谈论执行应用程序.(假设您无权访问源代码.)过去,一直保证如果在X版本上编译和执行的类,在版本X下编译的类文件将在版本Y上执行,如果Y> = X.(只要遵循某些基本规则,例如不使用com.sun类等).默认方法意味着应用程序供应商不再能够控制其编译的应用程序是否与未来的JVM版本兼容. (2认同)
  • "但如果没有编译问题,那么在Java 8上运行的应用程序将在Java 9上运行而没有任何问题." 区别在于*是*编译问题.例如,具有变量名"enum"的代码将在1.4下编译,但不会在1.5或更高版本下编译.但是,从这些代码在1.4下编译的*二进制文件仍将在JVM 1.5及更高版本*下运行.情况不再如此:这是一个巨大的差异. (2认同)
  • 我发现的最接近的是关于你所关联的后卫方法的Goetz文章:"我们可以选择放松运行时语义,使程序更加强大,防止因不一致的单独编译引入的冲突,因为这种危险可能会非常无辜地发生. class实现了两个独立维护的库的接口." 这听起来有点像"我们知道这是一个问题,我们正在研究它." 这并不完全令人欣慰. (2认同)