向接口引入默认方法真的可以保持向后兼容性吗?

Posted

技术标签:

【中文标题】向接口引入默认方法真的可以保持向后兼容性吗?【英文标题】:Does introducing a default method to an interface really preserve back-compatibility? 【发布时间】:2014-03-24 19:11:45 【问题描述】:

我想我对在 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)

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

【问题讨论】:

当然可以。它尽可能地降低了风险,但是是的。 向接口添加默认方法(至少在核心库中)是“一次性”的想法也是如此:“我们将在 Java 8 中进行所有这些库更改,但是在 Java 9 中不再添加默认方法”?因为否则只是将 VM 从 Java 8 升级到 Java 9 可能会导致各种现有代码失败:这将是 Java 哲学的巨大变化,不是吗? 【参考方案1】:

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

所以,兼容性实际上是关于二进制兼容性。 JLS §13.5.6 - Interface Method Declarations 中对此进行了解释:

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

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

你得到IncompatibleClassChangeError的原因可能是因为在B接口中添加默认方法后,你没有重新编译你的C类。

另见:

Compatibility Guide for JDK 8。

【讨论】:

但这意味着如果将新的默认方法添加到接口中,比如在 Java 9 核心库中,那么当我升级时任何 Java 应用程序都可能会中断?并且“通过引入默认方法发生意外冲突的风险类似于向非最终类添加新方法的风险”根本不正确,是吗?向类添加方法不会导致以前可以执行的现有二进制文件现在无法执行。 "你得到 IncompatibleClassChangeError 的原因可能是因为你在 B 接口中添加了默认方法后没有重新编译你的 C 类。"当然......它不会按原样编译。但是假设 C 类是我从供应商处购买的应用程序中的一个类,并且在核心库中进行了更改...... 不,我不是在谈论编译,我是在谈论执行应用程序。 (假设您无权访问源代码。)过去,一直保证如果一个类在版本 X 上编译和执行,则在版本 X 下编译的类文件将在版本 Y 上执行,如果 Y>= X。 (只要您遵循某些基本规则,例如不使用 com.sun 类等)默认方法意味着应用程序供应商不再控制其编译的应用程序是否与未来的 JVM 版本兼容。 "但是如果没有编译问题,那么在 Java 8 上运行的应用程序将在 Java 9 上运行而没有任何问题。"不同之处在于存在 编译问题的情况。例如,变量名为“enum”的代码将在 1.4 下编译,但在 1.5 或更高版本下无法编译。但是,从此类代码在 1.4 下编译的二进制文件仍可在 JVM 1.5 及更高版本下运行。情况已不再如此:这是一个巨大的差异。 我发现的最接近的是在 Goetz 关于您链接的防御方法的文章中:“我们可能会选择放宽运行时语义,以使程序对不一致的单独编译引入的冲突更加健壮,因为这当一个类从两个单独维护的库中实现接口时,危险可能会非常天真地发生。”这听起来有点像“我们知道这是一个问题,我们正在努力解决这个问题。”这并不令人欣慰。【参考方案2】:

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

例如你可以像这样修复一个类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() 

如果只重新编译接口J,方法T::m 仍将委托给I::m。但是T 本身的编译将不再成功——它会以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
    

【讨论】:

以上是关于向接口引入默认方法真的可以保持向后兼容性吗?的主要内容,如果未能解决你的问题,请参考以下文章

默认方法

Java 8 默认方法作为特征:安全吗?

在 iOS 6 中启用自动布局,同时保持向后兼容 iOS 5

如何增加导航控制器向后滑动手势响应区域?

接口的默认方法

我可以添加具有向后兼容性的 [FromBody] 参数吗?