改造void方法以返回其论点以促进流畅性:打破变化?

pol*_*nts 14 java jvm api-design binary-compatibility

"API设计就像性:做出一个错误并在你的余生中支持它" (Josh Bloch在Twitter上)

Java库中存在许多设计错误.Stack extends Vector(讨论),我们无法在不造成破损的情况下解决这个问题.我们可以尝试弃用Integer.getInteger(讨论),但它可能会永远存在.

尽管如此,某些类型的改装可以在不造成破损的情况下完成.

有效的Java第2版,第18项:首选接口到抽象类:现有的类可以很容易地进行改进,以实现新的接口".

例如:String implements CharSequence,Vector implements List,等.

有效的Java第2版,第42项:明智地使用varargs:您可以改进现有方法,该方法将数组作为其最终参数,而不是对现有客户端采取varags.

一个着名的例子是Arrays.asList引起混淆(讨论),但没有破坏.

这个问题是关于不同类型的改造:

您是否可以在void不破坏现有代码的情况下改进返回方法的方法?

我最初的预感指向是,因为:

  • 返回类型不会影响在编译时选择哪个方法
  • 即使你使用反射,Class.getMethod也不会在返回类型上区分

但是,我希望听到其他在Java/API设计方面经验丰富的人进行更全面的分析.


附录:动机

正如标题中所建议的那样,一个动机是促进流畅的界面风格编程.

考虑这个简单的代码片段,它打印一个混洗的名称列表:

    List<String> names = Arrays.asList("Eenie", "Meenie", "Miny", "Moe");
    Collections.shuffle(names);
    System.out.println(names);
    // prints e.g. [Miny, Moe, Meenie, Eenie]
Run Code Online (Sandbox Code Playgroud)

已经Collections.shuffle(List)被宣布为返回输入列表中,我们可以这样写:

    System.out.println(
        Collections.shuffle(Arrays.asList("Eenie", "Meenie", "Miny", "Moe"))
    );
Run Code Online (Sandbox Code Playgroud)

还有其他方法Collections那会显得更愉快,如果他们要返回输入列表,而不是使用void,例如reverse(List),sort(List)等等.事实上,拥有Collections.sortArrays.sort回报void是特别不幸的,因为它剥夺了我们从编写代码的表现等如下:

// DOES NOT COMPILE!!!
//     unless Arrays.sort is retrofitted to return the input array

static boolean isAnagram(String s1, String s2) {
    return Arrays.equals(
        Arrays.sort(s1.toCharArray()),
        Arrays.sort(s2.toCharArray())
    );
}
Run Code Online (Sandbox Code Playgroud)

当然,这种void防止流畅性的返回类型不仅限于这些实用方法.这些java.util.BitSet方法也可以写成返回this(ala StringBufferStringBuilder)以促进流畅性.

// we can write this:
    StringBuilder sb = new StringBuilder();
    sb.append("this");
    sb.append("that");
    sb.insert(4, " & ");
    System.out.println(sb); // this & that

// but we also have the option to write this:
    System.out.println(
        new StringBuilder()
            .append("this")
            .append("that")
            .insert(4, " & ")
    ); // this & that

// we can write this:
    BitSet bs1 = new BitSet();
    bs1.set(1);
    bs1.set(3);
    BitSet bs2 = new BitSet();
    bs2.flip(5, 8);
    bs1.or(bs2);
    System.out.println(bs1); // {1, 3, 5, 6, 7}

// but we can't write like this!
//  System.out.println(
//      new BitSet().set(1).set(3).or(
//          new BitSet().flip(5, 8)
//      )
//  );
Run Code Online (Sandbox Code Playgroud)

不幸的是,不像StringBuilder/ StringBuffer,所有BitSet的存取器返回void.

相关话题

pol*_*nts 14

不幸的是,是的,更改void方法以返回某些内容是一个重大变化.此更改不会影响源代码兼容性(即,相同的Java源代码仍将像以前一样编译,完全没有明显的效果)但它会破坏二进制兼容性(即先前针对旧API编译的字节码将不再运行).

以下是Java语言规范第3版的相关摘录:

13.2二进制兼容性是什么和不是什么

二进制兼容性与源兼容性不同.


13.4类的演变

本节描述更改对类及其成员和构造函数的声明对预先存在的二进制文件的影响.

13.4.15方法结果类型

更改方法的结果类型,将结果类型替换为结果类型void或替换void为结果类型具有以下组合效果:

  • 删除旧方法,和
  • 使用新结果类型或新void结果添加新方法.

13.4.12方法和构造函数声明

从类中删除方法或构造函数可能会破坏与引用此方法或构造函数的任何预先存在的二进制文件的兼容性 ; 一NoSuchMethodError当从预先存在的二进制这样的参考链接可能抛出.仅当在超类中未声明具有匹配签名和返回类型的方法时,才会发生此类错误.

也就是说,虽然Java编译器在方法解析过程中在编译时忽略了方法的返回类型,但此信息在JVM字节码级别的运行时很重要.


在字节码方法描述符上

方法的签名不包括返回类型,但其字节码描述符包含返回类型.

8.4.2方法签名

如果两个方法具有相同的名称和参数类型,则它们具有相同的签名.


15.12方法调用表达式

15.12.2编译时步骤2:确定方法签名

该描述符(签名加上返回类型)的最具体方法是在运行时用于执行方法分派.

15.12.2.12示例:编译时解决方案

在编译时选择最适用的方法; 它的描述符确定在运行时实际执行的方法.

如果将新方法添加到类中,则使用旧类定义编译的源代码可能不会使用new方法,即使重新编译会导致选择此方法.

理想情况下,只要更改了依赖的代码,就应该重新编译源代码.但是,在不同组织维护不同类的环境中,这并不总是可行的.

对字节码进行一点检查有助于澄清这一点.当javap -c在名称shuffling片段上运行时,我们会看到如下指令:

invokestatic java/util/Arrays.asList:([Ljava/lang/Object;)Ljava/util/List;
             \______________/ \____/ \___________________/\______________/
                type name     method      parameters        return type

invokestatic java/util/Collections.shuffle:(Ljava/util/List;)V
             \___________________/ \_____/ \________________/|
                   type name        method     parameters    return type
Run Code Online (Sandbox Code Playgroud)

相关问题


关于不破坏改造

现在让我们解决为什么改进new interface或vararg的原因,如Effective Java 2nd Edition中所述,不会破坏二进制兼容性.

13.4.4超类和超级接口

更改直接超类或类类型的直接超接口集不会破坏与预先存在的二进制文件的兼容性,前提是类类型的超类或超接口的总集合不会丢失任何成员.

对新进行改造interface不会导致类型丢失任何成员,因此这不会破坏二进制兼容性.同样,由于varargs是使用数组实现的,因此这种改进也不会破坏二进制兼容性.

8.4.1形式参数

如果最后一个形式参数是类型的变量arity参数T,则认为它定义了类型的形式参数T[].

相关问题


绝对没办法做到这一点吗?

实际上,是的,有一种方法可以改进先前void方法的返回值.我们不能有两个方法与Java源代码级别完全相同的签名,但我们CAN有JVM级别,只要它们有不同的描述符(由于有不同的返回类型).

因此,我们可以提供二进制例如java.util.BitSet,其具有同时具有void和非void返回类型的方法.我们只需将非void版本发布为新API.实际上,这是我们可以在API上发布的唯一内容,因为在Java中使用具有完全相同签名的两种方法是非法的.

该解决方案是一个可怕的黑客,需要特殊(和规格玩命)处理编译BitSet.javaBitSet.class,因此,它可能不值得努力这样做.