【问题标题】:Java binary compatibility - RFC on proposed solution to covariant return type using invokevirtual semanticsJava 二进制兼容性 - RFC 关于使用 invokevirtual 语义对协变返回类型提出的解决方案
【发布时间】:2010-08-17 06:53:35
【问题描述】:

我正在尝试开发 API。作为这种演变的一部分,我需要将方法的返回类型更改为子类(专业化),以便高级客户能够访问新功能。 示例(忽略丑陋的:

public interface Entity {
  boolean a();
}

public interface Intf1 {
  Entity entity();
}

public interface Main {
  Intf1 intf();
}

我现在想要像这样拥有 ExtendedEntity、Intf2 和 Main:

public interface ExtendedEntity extends Entity {
  boolean b();
}

public interface Intf2 extends Intf1 {
  ExtendedEntity entity();
}

public interface Main {
  Intf2 intf();
}

但是,由于方法返回类型是其签名的一部分,因此已经使用以前版本的代码编译的客户端会显示链接错误(找不到方法 iirc)。

我想做的是向 Main 添加一个具有不同返回类型的方法。这两种方法(一种返回超类型,一种返回子类型)应该映射到相同的实现方法(返回子类型)。注意 - 据我了解,JVM 允许这样做,但 Java 规范不允许这样做。

我的解决方案,似乎会滥用(我对此无话可说)Java 类系统来添加所需的接口。

public interface Main_Backward_Compatible {
  Intf1 intf();
}

public interface Main extends Main_Backward_Compatible{
  Intf2 intf();
}

现在旧客户端将有正确的方法返回到 invokevirtual 查找(因为具有正确返回类型的方法存在于类型层次结构中)并且实际工作的实现将是返回子类型 Intf2 的实现。

似乎有效。在我可以设计的所有测试中(除了反射 - 但我不关心那一点)它确实工作。
它会一直有效吗?我的推理(关于调用虚拟)是否正确?

还有一个相关的问题——是否有工具可以检查“真正的”二进制兼容性?我发现的唯一方法是单独查看每种方法,但没有考虑类型层次结构。

谢谢,
冉。

编辑 - 我尝试并发现“不太好”的工具(不考虑类型层次结构):

  1. Clirr 0.6.
  2. IntelliJ“APIComparator”插件。

Edit2 - 当然,我的客户被禁止为我的接口创建实现类(想想服务)。但是,如果您希望示例完整,请考虑抽象类(用于 Main)而不是接口。

【问题讨论】:

    标签: java compiler-construction binary-compatibility


    【解决方案1】:

    这已经够长了,我承认我没有仔细阅读所有内容,但看起来你可能真的想在这里利用泛型。如果你输入Intf1,我认为你可以在引入特化的同时保持二进制兼容性:

    public interface Intf1<T extends Entity> {
      T entity(); //erasure is still Entity so binary compatibility
    }
    
    public interface Intf2 extends Intf1<ExtendedEntity> { //if even needed
    }
    
    public interface Main {
      Intf1<ExtendedEntity> intf(); //erasure is still Intf1, the raw type
    }
    

    编辑#1:在尝试保持二进制兼容性时有一些注意事项。有关详细信息,请参阅Generics Tutorial 第 6 章和第 10 章。

    编辑#2:

    您也可以将此概念扩展到输入Main

    public interface Main<T, I extends Intf1<T>> {
        I intf(); //still has the same erasure as it used to, so binary compatible
    }
    

    然后,旧客户端将能够像过去一样使用原始 Main 类型,而无需重新编译,而新客户端将键入对 Main 的引用:

    Main<ExtendedEntity, Intf2> myMain = Factory.getMeAMain();
    Intf2 intf = myMain.intf();
    

    【讨论】:

    • 我不能这样做,因为我之前没有想到这一点——现在 Intf1 已经在公共 API 中了。关于新接口-我事先不知道要参数化什么-所以下次我需要影响更改时-我会遇到同样的问题。另外,我不能将 Intf1 改造为泛型,因为这违反了二进制兼容性(您可以更改泛型参数,但不能更改类是否参数化的事实)。
    • @Ran:我很确定你错了。例如,当集合 API 在 Java 1.5 中使用泛型进行改造时,无需重新编译所有现有代码。所以是的,我认为你错了。有关此问题的讨论,请参阅 java.sun.com/j2se/1.5/pdf/generics-tutorial.pdf(泛型教程)(特别是第 6.3 和 10 节)。
    • @Mark:很有趣。我将不得不针对其他解决方案考虑这一点(与往常一样,实际代码比玩具示例更复杂——我确实需要返回 Intf2,因为我正在向它添加方法)。但是你肯定得到了我的 +1 :)
    • @Ran:我会在该注释上添加更多内容。至于泛型是否适合您,取决于您的实际情况。您不应该仅仅因为它们使您尝试做的事情成为可能......特别是在 API 中......但如果类层次结构看起来应该使用泛型无论如何,这是一种前进的方式。
    • @Mark:我现在才想到——如果我使用泛型,我会破坏源代码兼容性。这对我来说没有破坏二进制兼容性那么严重,但仍然令人担忧。无论如何,这是个好主意。
    【解决方案2】:

    我们最终不需要解决方案,但在此之前证明它有效。

    【讨论】:

      【解决方案3】:

      根本不更改现有接口会更简单。无论如何,任何使用您的新界面的人都将编写新代码。

      现有 Main.intf() 签名的实现可以返回 Intf2 的实例。

      或者,您可以提供一个不需要强制转换的新访问器:

      public interface Main2 extends Main {
        Intf2 intf2();
      }
      

      【讨论】:

      • 这只是将问题更进一步 - Main(或 Main2)需要从某个地方提供服务。的确,我可以一直向上传播——但这涉及大量接口,它们都与变化无关(工厂、服务、更多工厂、更多服务......)
      • 您可以从声明为返回 Intf1 的方法返回 Intf2 的实例。您可以避免修改现有接口。
      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2015-04-05
      • 1970-01-01
      • 2010-10-20
      • 1970-01-01
      • 1970-01-01
      • 2011-09-02
      相关资源
      最近更新 更多