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

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

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

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

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

示例: String implements CharSequenceVector implements List等。

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

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

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

你能改造一个void方法来返回一些东西而不破坏现有的代码吗?

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

  • 返回类型不会影响在编译时选择哪个方法
    • 请参阅: JLS 15.12.2.11 – 不考虑返回类型
    • 因此,更改返回类型不会更改编译器选择的方法
    • void改造返回一些东西是合法的(但不是相反的方式!)
  • 即使使用reflection, Class.getMethod东西也不区分返回类型

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


附录:动机

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

考虑这个简单的片段,它打印一个洗牌的名单:

  List names = Arrays.asList("Eenie", "Meenie", "Miny", "Moe"); Collections.shuffle(names); System.out.println(names); // prints eg [Miny, Moe, Meenie, Eenie] 

如果已将Collections.shuffle(List)声明为返回输入列表,我们可以写:

  System.out.println( Collections.shuffle(Arrays.asList("Eenie", "Meenie", "Miny", "Moe")) ); 

如果他们要返回输入列表而不是void ,例如reverse(List)sort(List)等,那么Collections中还有其它方法可以使用得更加愉快。事实上,拥有Collections.sortArrays.sort return 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()) ); } 

当然,这种防止流畅性的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) // ) // ); 

不幸的是,与StringBuilder / StringBuffer不同, BitSet所有 mutator都返回void

相关话题

  • 方法链 – 为什么这是一个好的做法?

不幸的是,是的,改变一个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 

相关问题

  • java:这是什么: [Ljava.lang.Object;

关于不破坏改造

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

13.4.4超类和超级接口

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

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

8.4.1forms参数

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

相关问题

  • forms参数类型声明中double…double[]之间的区别

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

实际上,是的,有一种方法可以改进以前void方法的返回值。 我们不能在Java源代码级别有两个具有相同精确签名的方法,但我们可以在JVM级别具有该方法,前提是它们具有不同的描述符(由于具有不同的返回类型)。

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

这个解决方案是一个糟糕的黑客,需要特殊的(和规范的)处理来将BitSet.class编译为BitSet.class ,因此可能不值得这样做。

如果你不能改装,你仍然可以将你的类包装成一个新的类,它使用相同的方法,但返回正确(MyClassFluent)。 或者您可以使用不同的名称添加新方法,而不是Arrays.sort()我们可以使用Arrays.getSorted()

我认为解决方案不是强迫事情,只是处理它们。

编辑:我知道我没有回答“改造无效方法”的问题,但你的回答已经很清楚了。

如果你只需要处理源兼容性,那就继续吧。 从void更改为返回类型不会中断。

但是要解决你真正想要做的事情:我认为流畅的接口问题是线条往往变得相当长,并且 – 在格式化之后 – 有些不可读。 对于建设者来说,它工作得很好,但我可能不会将它用于其他任何事情。

这是为了玩它,还是因为你发现这真的很棒?